Skip to content

Websites / Cloudflare

Site Migration Runbook

Platform
Websites / Cloudflare
Owner
Website Specialist
Assignee
Jakob
Supports
SEO Specialist
Needs review — This SOP contains our content but has not been verified by Nick. Treat as a working draft until marked Live.

Last Updated: 2026-05-01 Version: 1.0 Tier: Per-client cutover playbook (mandatory for every DNS flip)


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.


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:#fff

Phase 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.

TypeTriggerCoordination needed
SilentTekton controls the registrar (we registered the domain or have full account access)None beyond final client approval
PermissionedClient 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.

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.

Terminal window
dig <domain> +noall +answer
dig <domain> MX +noall +answer
dig <domain> TXT +noall +answer

You need to capture: A, AAAA, CNAME, MX, TXT (SPF/DKIM/DMARC), and any subdomains in use.

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 _domainkey subdomain
  • DMARC record — TXT record at _dmarc.<domain>
  • Email subdomainsmail.<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.

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:

  1. Domain-locked vendor iframes are usually broken on the <slug>.pages.dev test URL because vendors allowlist by referrer/domain (TrustIndex, GHL forms, Google Maps keys, etc.). /clone-gate hides these via CSS overrides in public/css/_clone-overrides.css so the test URL doesn’t show empty broken sections. The postbuild script auto-strips those override rules when the production domain is set in site-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.

  2. 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 captured head.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.xml or intentional /sitemap-index.xml generated from the approved live page inventory, not a blind default Astro or CMS export
Terminal window
# Source: fetch the index, then fetch each child sitemap
curl -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 build
curl -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 diffing
sed -i '' 's|https://[^/]*||' /tmp/source-urls.txt /tmp/new-urls.txt
diff /tmp/source-urls.txt /tmp/new-urls.txt

For 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.

  • 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.com should 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-verify skill handles this end-to-end)
  • Conversion events configured (see Site Launch Gate SOP)

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.


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.

In the Cloudflare dashboard:

  1. Pages → <client-slug> → Custom domains → Add a domain
  2. Enter the production domain (example.com and www.example.com)
  3. 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.

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.

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

Once DNS is propagating:

  1. Site loads — open production URL in incognito. New site should appear.
  2. 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.
  3. 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.
  4. Vendor widgets visible — if CUTOVER.md listed 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.
  5. Tracking fires — open browser DevTools → Network → filter collect. Reload page. Confirm a collect request to Google Analytics with the right measurement ID.
  6. Click 5 minutes of internal links — random sample. None should 404. Click any image gallery; lightboxes should load full-size images.

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 from Disallow: / to Allow: / + adds the final custom production sitemap URL
  • dist/_headers: drops the X-Robots-Tag: noindex, nofollow, noarchive rule
  • 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/_redirects or 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 has x-robots-tag in headers
  • Confirm curl https://<domain>/robots.txt returns User-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, submit https://<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.

  • 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.


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:

  1. Open the DNS record backup from Phase 1.2
  2. At the registrar, restore the original A/CNAME records (keep all MX/TXT untouched if you preserved them correctly)
  3. Wait for propagation (usually faster on the way back since old TTL is shorter)
  4. Tell Nick what happened
  5. 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.


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_lead events firing
  • GSC property is Verified (was verified pre-flip per Phase 1.6), sitemap-index.xml submitted, 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

  • v1.1 (2026-05-08): Updated based on Tierra Design clone-gate findings (first real cutover candidate). Added: per-client CUTOVER.md cross-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 everything postbuild-seo.mjs does 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.