Websites / Cloudflare
Site Migration Runbook
SOP: Site Migration Runbook
Section titled “SOP: Site Migration Runbook”Last Updated: 2026-05-01 Version: 1.0 Tier: Per-client cutover playbook (mandatory for every DNS flip)
Purpose
Section titled “Purpose”The end-to-end procedure for taking a single client from their old hosting (Squarespace, GoHighLevel, WordPress, etc.) to the new Astro on Cloudflare Pages stack. Done wrong, you can break their email, kill their SEO rankings, or take their site down for a day. Done right, the client doesn’t notice anything except a faster site.
This SOP covers the day-of-cutover operation. Pre-cutover QA is in Pre-Cutover Visual Diff. Onboarding fundamentals are in Web Designer Onboarding & Handoff.
When to Use:
- After the test URL has been GO’d by the client in writing
- After the visual diff SOP returns zero P0 + zero P1 findings
- For each individual client cutover (run this top-to-bottom every time)
Owner: Web Designer (executes), CSM (client comms), Nick (final approval on first 2 pilots) Timeline: 30 minutes active work + up to 24 hours for DNS propagation. Block 4 hours on the calendar — you don’t want to be rushed.
The migration in one diagram
Section titled “The migration in one diagram”flowchart TD Start([Test URL approved by client]) --> Type{Silent or<br/>permissioned?} Type -->|Tekton controls registrar| Silent[Silent migration<br/>no client coordination needed] Type -->|Client controls registrar| Perm[Permissioned migration<br/>30-min window with client]
Silent --> Pre[Pre-flight checklist<br/>1 week before cutover] Perm --> Pre
Pre --> Day[Cutover day:<br/>add custom domain in CF<br/>flip DNS<br/>preserve MX records] Day --> Validate[Validate post-flip:<br/>site loads, email works,<br/>forms submit, tracking fires] Validate --> Monitor[72-hour monitoring] Monitor --> Done([Migration complete])
Validate -.->|something broken| Rollback[Rollback DNS<br/>tell Nick]
style Pre fill:#fbbf24,color:#000 style Day fill:#fbbf24,color:#000 style Validate fill:#fbbf24,color:#000 style Done fill:#22c55e,color:#fff style Rollback fill:#dc2626,color:#fffPhase 1 — Pre-flight (run 1 week before cutover)
Section titled “Phase 1 — Pre-flight (run 1 week before cutover)”Don’t start the cutover day if any of these are missing.
1.1 Confirm migration type
Section titled “1.1 Confirm migration type”| Type | Trigger | Coordination needed |
|---|---|---|
| Silent | Tekton controls the registrar (we registered the domain or have full account access) | None beyond final client approval |
| Permissioned | Client controls the registrar (GoDaddy, Squarespace, Network Solutions, Bluehost, etc.) | OTP forwarding — client must be available |
To find out: check the client inventory spreadsheet. If unclear, run whois <domain> and see what registrar comes up.
1.2 Document current DNS state (backup)
Section titled “1.2 Document current DNS state (backup)”Before changing anything, screenshot or export the current DNS record set. Save to ~/Projects/seo-ops-skills/logs/<client>-dns-before.txt. This is your rollback baseline.
dig <domain> +noall +answerdig <domain> MX +noall +answerdig <domain> TXT +noall +answerYou need to capture: A, AAAA, CNAME, MX, TXT (SPF/DKIM/DMARC), and any subdomains in use.
1.3 Confirm email infrastructure
Section titled “1.3 Confirm email infrastructure”Email runs off DNS records. If we mess them up, the client loses their inbox.
Capture and confirm:
- MX records — point to email provider (Google Workspace =
*.google.com, Microsoft 365 =*.outlook.com, hosting-bundled = registrar’s own) - SPF record — TXT record starting
v=spf1 - DKIM record — TXT record at a
_domainkeysubdomain - DMARC record — TXT record at
_dmarc.<domain> - Email subdomains —
mail.<domain>,webmail.<domain>,smtp.<domain>— all must be preserved
Rule: when you flip DNS, you replace ONLY the A record (and root CNAME). Every other record stays exactly as it was. Email continuity depends on this.
1.4 Inventory third-party integrations
Section titled “1.4 Inventory third-party integrations”Walk the source site. Document anything that pulls in external services:
- Payment processors (Stripe, Square)
- Booking widgets (Calendly, Acuity, Schedulicity)
- Live chat (Intercom, Drift, Tawk, GHL chat)
- Review widgets (TrustIndex, Birdeye)
- Form widgets (GHL embeds, Gravity Forms, Typeform, Wufoo)
- Analytics scripts beyond GA4
- Map embeds (Google Maps, Mapbox)
- Any API integrations
For each: confirm it works on the test URL OR document it as known-broken-post-cutover.
Two specific patterns to expect on every clone-gate site:
-
Domain-locked vendor iframes are usually broken on the
<slug>.pages.devtest URL because vendors allowlist by referrer/domain (TrustIndex, GHL forms, Google Maps keys, etc.)./clone-gatehides these via CSS overrides inpublic/css/_clone-overrides.cssso the test URL doesn’t show empty broken sections. The postbuild script auto-strips those override rules when the production domain is set insite-config.json. So they “should just work” post-cutover — but the vendor’s allowlist may need updating if the production domain isn’t already on it. Confirm each vendor’s dashboard has the production domain in its allowed origins BEFORE the flip if you can. -
Tekton’s searchatlas Otto plugin (loaded via
seodashboard.tektongrowth.com/scripts/dynamic_optimization.js) does client-side title + meta rewrites per UUID per domain. The UUID is on the source page as<script id="sa-dynamic-optimization" data-uuid="...">. Find the UUID in the capturedhead.html, confirm with the Tekton SEO team that it’s configured for the new production domain, OR file a ticket to update it. Without this step, post-flip the production domain serves SSR Yoast titles that may differ from what Otto was rewriting source titles to.
1.5 Audit URL parity and build the launch sitemap plan
Section titled “1.5 Audit URL parity and build the launch sitemap plan”Every valuable URL on the source site must either:
- Exist on the new live site as an indexable canonical page, OR
- Have a 301 redirect mapped from old → new
Mismatches kill SEO rankings. Use the old CMS sitemap only as an input for parity checking, not as the sitemap that gets shipped. For launch, build a fully custom production sitemap from the exact pages approved to go live. The final sitemap should help search engines understand the hierarchy of the new site, not preserve old CMS clutter.
Sitemap paths vary by stack:
- Source (WordPress/Yoast usually):
/sitemap_index.xml→ list of child sitemaps (page-sitemap.xml, post-sitemap.xml, etc.) - Launch sitemap (Tekton standard): a custom
/sitemap.xmlor intentional/sitemap-index.xmlgenerated from the approved live page inventory, not a blind default Astro or CMS export
# Source: fetch the index, then fetch each child sitemapcurl -s https://<source-domain>/sitemap_index.xml \ | grep -oE '<loc>[^<]+</loc>' \ | sed 's/<\/*loc>//g' \ | xargs -I {} curl -s {} \ | grep -oE '<loc>[^<]+</loc>' \ | sed 's/<\/*loc>//g' \ | sort -u > /tmp/source-urls.txt
# New launch sitemap: use the custom sitemap URL on the preview or production-equivalent buildcurl -s https://<slug>.pages.dev/sitemap.xml \ | grep -oE '<loc>[^<]+</loc>' \ | sed 's/<\/*loc>//g' \ | sort -u > /tmp/new-urls.txt
# If the custom sitemap is an index, fetch each child sitemap first, then extract locs.
# Normalize hostnames before diffingsed -i '' 's|https://[^/]*||' /tmp/source-urls.txt /tmp/new-urls.txtdiff /tmp/source-urls.txt /tmp/new-urls.txtFor each missing source URL: decide whether it deserves a live page or a 301. Add the page if it is a real ranking/service URL. Add a redirect rule in public/_redirects when the old path should consolidate into a new canonical URL (Cloudflare Pages format: /old-path /new-path 301) before cutover. After adding redirects, check that old URLs land on the correct new canonical page.
If the new sitemap has URLs the source doesn’t, that can be fine if they are approved live pages, but verify they are not thin, duplicate, noindexed, or accidentally generated utility routes.
1.6 Confirm tracking access
Section titled “1.6 Confirm tracking access”- GSC ownership BEFORE the flip — verify the production domain in Google Search Console before DNS changes, using the DNS TXT method (not file-upload, which only works after the new site is live). Tekton’s
seo-brain@core-depth-472801-t2.iam.gserviceaccount.comshould already be added as an Owner on the existing source property; if not, add it now. Post-flip, the only remaining GSC step is “submit sitemap” — verification is already done. - GA4 property exists and is wired into the clone (
/gsc-verifyskill handles this end-to-end) - Conversion events configured (see Site Launch Gate SOP)
1.7 Get written client approval
Section titled “1.7 Get written client approval”Email or signed handoff doc. “Looks good” in a DM is not enough. You’re about to make a change that costs real money to undo.
The approval message should reference:
- The exact test URL they reviewed
- Confirmation they tested forms, navigation, mobile
- Acknowledgement that the cutover may take up to 24 hours to propagate fully
- A scheduled time window for the cutover
1.8 (Permissioned only) Confirm OTP forwarding window
Section titled “1.8 (Permissioned only) Confirm OTP forwarding window”If the client controls the registrar, you need them at their inbox during the cutover. Most registrars (GoDaddy, Squarespace, Network Solutions) require an emailed OTP code to approve DNS changes. The code typically expires in 10 minutes.
Send the client this template the day before:
Hey [name] — confirming our [time] cutover for [domain]. When I submit the DNS change, [registrar] will email YOU an OTP code (8-digit number, subject line “Verify your domain change” or similar). Just forward that email to me as soon as it lands. Plan to be at your inbox between [time] and [time + 30 min].
Save the planned start time to your calendar. Include client’s preferred contact method (text? Slack?) in the calendar invite.
Phase 2 — Cutover day
Section titled “Phase 2 — Cutover day”2.0 Email round-trip baseline (before touching anything)
Section titled “2.0 Email round-trip baseline (before touching anything)”Send a test email TO the client’s domain (use a Gmail you control: jakob+pre-cutover@tektongrowth.com → <contact-email>@<client-domain>) and ask the client to reply. Both should land within 5 minutes. This is your “email already worked” baseline. If email is broken AFTER the flip, this lets you tell apart “we broke it” from “it was already broken.” Note the timestamp of the round-trip in the per-client log.
2.1 Add custom domain in Cloudflare Pages
Section titled “2.1 Add custom domain in Cloudflare Pages”In the Cloudflare dashboard:
- Pages →
<client-slug>→ Custom domains → Add a domain - Enter the production domain (
example.comandwww.example.com) - Cloudflare gives you the DNS record to point at — usually a CNAME to
<slug>.pages.dev
Do NOT update DNS yet. Cloudflare will show “pending” status until DNS resolves.
2.2 Flip DNS at the registrar
Section titled “2.2 Flip DNS at the registrar”This is the actual moment of cutover.
Records to update:
- A record
@→ CNAME (or A) → Cloudflare Pages target - A record
www→ same target
Records to PRESERVE EXACTLY:
- All MX records
- SPF (TXT starting
v=spf1) - DKIM (TXT at
*._domainkey) - DMARC (TXT at
_dmarc) - Email subdomains (
mail.,webmail., etc.) - Any subdomains pointing at services we’re not touching
For permissioned migrations: when you click Save, the registrar emails the OTP. Watch the time. The client should forward it to you within 10 minutes. Enter it. Confirm save.
2.3 Wait for propagation
Section titled “2.3 Wait for propagation”Most updates propagate within a few minutes. Use https://dnschecker.org to monitor:
- Enter the production domain
- Look for the new A/CNAME target spreading across DNS servers worldwide
- Don’t panic until you’ve waited 30 minutes
2.4 Validate the live site
Section titled “2.4 Validate the live site”Once DNS is propagating:
- Site loads — open production URL in incognito. New site should appear.
- Email works — send a test email TO the client domain (use a Gmail you control). Then ask client to send one back. Both should land within 5 minutes. Compare against your pre-flip baseline from 2.0; same latency = email is healthy.
- Forms render AND submit — for every form on every page:
- First, visually confirm the form actually appears on the page (no blank section). On clone-gate sites, the form iframe was hidden by CSS overrides; the postbuild auto-strips those when domain is set, so the form should be visible. If it’s STILL blank: the form vendor’s allowlist needs the new domain added (check the iframe network request in devtools — 403/X-Frame errors mean vendor-side allowlist rejection).
- Then submit a tagged test entry (e.g.
firstname: Jakob, email: jakob+<client>-cutover@tektongrowth.com). - Confirm the test entry lands in the right GHL location/pipeline within 60 seconds.
- Vendor widgets visible — if
CUTOVER.mdlisted any vendor widgets (TrustIndex reviews, Calendly, map embeds, chat widget): visually confirm each renders on the page where it’s expected. Empty/blank section = vendor allowlist issue, see step 3 pattern. - Tracking fires — open browser DevTools → Network → filter
collect. Reload page. Confirm acollectrequest to Google Analytics with the right measurement ID. - Click 5 minutes of internal links — random sample. None should 404. Click any image gallery; lightboxes should load full-size images.
2.5 Set production domain and redeploy
Section titled “2.5 Set production domain and redeploy”The clone was deployed in noindex mode. Flipping it to production is a single config change.
Edit src/data/site-config.json in the client repo. Change:
"deployment": { "domain": null }to:
"deployment": { "domain": "<production-domain>" }Use the canonical form (apex or www.) — pick one and stick with it; the other should redirect to it.
Commit + push. The GitHub Action redeploys. The scripts/postbuild-seo.mjs script that ships with every clone-gate repo automatically does ALL of the following on production builds — you should not edit any of these files by hand:
dist/robots.txt: flips fromDisallow: /toAllow: /+ adds the final custom production sitemap URLdist/_headers: drops theX-Robots-Tag: noindex, nofollow, noarchiverule- Custom sitemap output: builds from the approved live page inventory with production canonical URLs and clear hierarchy. Do not ship stale CMS sitemap files or a blind default Astro sitemap as the final launch sitemap.
- Redirect map: keeps any needed old → new 301s in
public/_redirectsor the approved middleware layer, then verifies them after deploy. dist/css/_clone-overrides.css: strips the rules that hide vendor iframes (TrustIndex, GHL form, etc.) so they reappear
Or invoke via Claude:
“Run /site-cutover for ferran-landscape. DNS is propagating. Set deployment.domain to ferranlandscape.com, push, then submit the new sitemap to GSC.”
After redeploy completes:
- Confirm
curl -I https://<domain>/no longer hasx-robots-tagin headers - Confirm
curl https://<domain>/robots.txtreturnsUser-agent: *\nAllow: / - In Google Search Console, the new property should already be Verified from Phase 1.6. If not, verify now.
- Fetch the live custom sitemap and confirm it contains only approved production URLs, uses the canonical host, and reflects the page hierarchy going live.
- Submit the final custom sitemap URL, usually
https://<domain>/sitemap.xml. If the repo intentionally uses a custom sitemap index, submithttps://<domain>/sitemap-index.xml. - Confirm 0 errors, 0 warnings on initial scan
Phase 3 — Post-cutover monitoring (72 hours)
Section titled “Phase 3 — Post-cutover monitoring (72 hours)”Don’t walk away. Check the site daily for 3 days.
Day 1 (within 4 hours of cutover)
Section titled “Day 1 (within 4 hours of cutover)”- Site loads correctly from your home internet, mobile data, and a VPN endpoint in another region
- Email round-trip works
- GSC shows the property as Verified
- GA4 Realtime shows traffic
- Check GSC for new crawl errors
- Check GA4 for any traffic anomaly (massive drop = something’s wrong)
- Spot-check 5 random inner pages on the live site
- Check GSC index coverage — most pages should be transitioning to “Indexed”
- Run
dig <domain>— confirm DNS is fully propagated (no old IPs anywhere) - Document any client-reported issues in the per-client log
After day 3 with no issues, the migration is complete.
Rollback procedure (only if catastrophic)
Section titled “Rollback procedure (only if catastrophic)”A rollback means flipping DNS back to the old hosting. Don’t do this for cosmetic issues — fix those forward. Roll back only if:
- Site is fully down (502, 503, blank page)
- Email is bouncing
- Forms are losing submissions
- More than 50% of pages 404
The rollback:
- Open the DNS record backup from Phase 1.2
- At the registrar, restore the original A/CNAME records (keep all MX/TXT untouched if you preserved them correctly)
- Wait for propagation (usually faster on the way back since old TTL is shorter)
- Tell Nick what happened
- File a postmortem in
~/Projects/seo-ops-skills/logs/<client>-rollback-<date>.md
Once the live site is rolled back and the immediate fire is out, debug what went wrong on the test URL. Don’t re-attempt cutover until you’ve reproduced and fixed the issue locally.
Definition of done
Section titled “Definition of done”A migration is complete when ALL of these are true:
- Production domain serves the new Astro site (incognito, multiple regions confirmed)
- Email round-trip works in both directions
- Every form on the site submits successfully (test contact created in GHL)
- GA4 Realtime shows traffic with
generate_leadevents firing - GSC property is Verified (was verified pre-flip per Phase 1.6),
sitemap-index.xmlsubmitted, 0 errors - noindex tag removed from production
- DNS propagated globally (dnschecker.org all green)
- 72-hour monitoring complete with no client complaints
- Per-client log updated with cutover summary + any quirks
- CSM (Kyle) notified that the cutover succeeded
Version Control
Section titled “Version Control”- v1.1 (2026-05-08): Updated based on Tierra Design clone-gate findings (first real cutover candidate). Added: per-client
CUTOVER.mdcross-reference at top; Astro sitemap-index.xml path corrections in Phase 1.5; vendor iframe + searchatlas Otto guidance in Phase 1.4; pre-flight GSC verification clarification in Phase 1.6; pre-flip email baseline as Phase 2.0; explicit form-renders-vs-submits split + vendor widget verification in Phase 2.4; expanded Phase 2.5 to enumerate everythingpostbuild-seo.mjsdoes automatically (so designers don’t manually edit dist files). - v1.0 (2026-05-01): Initial SOP. Built from first principles with the Ferran Landscape + General Paving Stone pilots in mind.