Onboarding a client

The full process, start to finish. Follow it in order — several steps depend on earlier ones, and the ordering of the DNS section in particular is what keeps a client's email working.

Roughly 90% of this is identical for every client. The genuinely bespoke part is the design; the backend contract is the same everywhere. Don't diverge it per client.

#How to use this document

Every section can be done two ways, and they produce the same result.

With Claude. Open a Claude Code chat in the workspace folder and paste the grey block from the step you're on. Claude does the work — creating records, setting configuration, running the checks — and you approve each action as it asks. You still make the decisions; you are not typing the commands.

By hand. Ignore the grey blocks and follow the numbered steps. They are complete on their own: every click path, field and value is written out.

The grey blocks are self-contained on purpose. You don't have to explain the project first, and you can paste them out of order.

What Claude cannot do, and will tell you so:

Everything else it can do with you watching.


#Who can do which parts

Almost all of this can be done by anyone with an agency login, member or admin: creating the client and site record, setting the Search Console URL, GA4 property and Business Profile location, importing pages, pulling data, recording the Pages project, and ticking the site live.

Two things need an admin, and both for the same reason — they can't be undone from the interface:

Everything else is fair game. If you hit a wall, that's what it will be.

#Before you start, decide which path

Migrating — the client already has a site and it has traffic. Use migration-kit. Keep the existing URLs.

Building fresh — new business, or an agreed redesign. Clone client-template.

The decision matters more than it looks, and there is a check that settles it: export the client's Search Console data first. If pages are ranking, you are migrating, whatever anyone called the job. On FRA we nearly launched a "new" site that would have 404'd 42 of 43 live URLs, including a homepage with 34,000 impressions.

#Survey what the old site does, not just what it says

A rebuild that copies the pages and nothing else quietly drops the parts people actually used. Before planning the build, walk the live site and write down:

Record all of it in the client's BUILD-BRIEF.md, §2b.



#1. Create the client in the platform

Paste this to do it with Claude:

I'm onboarding a new client to the Majestic One platform. Create the tenant and
site records for me in the dashboard at portal.majesticone.co.uk, then tell me the
PUBLIC_SITE_ID and PUBLIC_SITE_INGEST_KEY. Also set the lead notification email —
ask me what it should be. Client name, domain and notify address: <fill these in>

Dashboard → add the tenant (the client) → add the site. That produces the two values everything else needs: PUBLIC_SITE_ID and PUBLIC_SITE_INGEST_KEY.

Both are shown on /sites/<id>/setup. If you can't find them, that's the page.

Set notifyEmail on the site now, not later. Without it the contact form submits successfully and nobody is told. On a fire-safety or emergency-trade site that's the worst failure available — the enquiry is captured and never answered.

#2. Build the site

Fresh:

git clone https://github.com/majesticonemain-a11y/client-template.git <client-repo>
cd <client-repo>
npm install
cp .env.example .env        # fill SITE_ID + INGEST_KEY from step 1

Fill src/data/site.ts (name, NAP, services, areas), then npm run import to seed the platform's page records from it. Then restyle the components — that's the bespoke part.

Migrating: follow migration-kit/README.md. prerender.mjs crawls the live site, de-duplicates pages that render the same content under several URLs, and emits static HTML plus sitemap, robots and _redirects.

#What every client site repo must contain

Check these exist before you go further. They are all in client-template, so a site cloned from it starts correct — but a site that was built earlier, or adapted from somewhere else, drifts. FRA was missing one of them and nobody noticed until its preview URL was already live and indexable.

File What it does Symptom if absent
functions/_middleware.js 301s *.pages.dev → the real domain The preview URL serves a full duplicate of the client's site and Google can index it. Nothing looks wrong.
src/pages/robots.txt.ts Generates robots.txt from SITE_URL A hardcoded robots.txt advertises another client's sitemap. This happened to eleven sites at once.
scripts/verify-build.mjs Blocks the build on broken links, blank pages, cross-domain URLs Bad builds deploy silently.
public/_redirects Path redirects, including /sitemap.xml → /sitemap-index.xml Audit tools report "no sitemap". On a migration, every old ranking URL 404s.
src/pages/404.astro A real 404 Unknown paths return 200 with the homepage, so Google indexes infinite duplicates of it.
src/components/Analytics.astro Consent-gated GA4 No analytics, or worse, analytics firing before consent.

Two of those need a matching environment variable to do anything — CANONICAL_HOST for the middleware and PUBLIC_GA4_ID for analytics. The file being present is not the same as the feature working; see the env var table in step 5.

#Rules for this step

#3. Push to GitHub

Each client is its own repo under majesticonemain-a11y. GitHub Desktop or CLI, either is fine.

The repo is the website. Nothing exists anywhere else until it's pushed.

#4. Create the Cloudflare Pages project

Cloudflare → Workers & Pages → Create → Pages → Connect to Git → pick the client's repo.

The GitHub↔Cloudflare connection is already authorised at account level, so you're just picking a repo. If the new repo isn't listed, the GitHub app is scoped to selected repos — add it in GitHub → Settings → Applications → Cloudflare Pages.

Every repo now pins Node in .nvmrc, but Cloudflare reads NODE_VERSION and that wins. Set it anyway, and keep the two in step — if they ever disagree, the env var decides and .nvmrc becomes a comment.

The first build will fail, and that is correct. With no environment variables the site has no identity, so the content API returns nothing and the build stops with:

Content API returned nothing — set PUBLIC_SITE_ID / PUBLIC_SITE_INGEST_KEY (.env)

That guard exists so a misconfigured site fails loudly instead of deploying with no pages. The next step clears it.

Note the project name and put it in the platform. Cloudflare gives every project a <project>.pages.dev host, which serves a complete second copy of the client's site. On /sites/<id>/setup there's a Cloudflare Pages project field — enter the name exactly as Cloudflare shows it.

That's what lets the daily self-test confirm the copy redirects to the real domain once the site is live, rather than sitting there competing with the client for their own search terms. Without it the check can only warn that it doesn't know.

Watch the name: it is not always the repo name. The Big Conversion Company's repo is thebigconversioncompany-site but its Pages project is thebigconversioncompany; HAL's project is hal-construction, not hal-construction-site; the Dorset site's is client-renderer. Copy it off the Cloudflare page rather than assuming.

#5. Set the environment variables

The site can't build without these. Create the client in the platform first (§1) — that's where the first two come from, on /sites/<id>/setup.

Cloudflare → the Pages project → Settings → Environment variables → Production:

Variable Value
PUBLIC_SITE_ID From the platform
PUBLIC_SITE_INGEST_KEY From the platform
PUBLIC_PLATFORM_URL https://majestic-system-theone.vercel.app
CONTENT_API_URL Same as above
SITE_URL The client's real domain, e.g. https://soakawayinstallation.co.uk — set this even before launch, because the sitemap is generated from it
NODE_VERSION 20
PUBLIC_GA4_ID Once GA4 exists — optional, add later
PUBLIC_CONSENT_REQUIRED true unless agreed otherwise

Then Deployments → Retry deployment. It should build.

CANONICAL_HOST is deliberately not in that list — it belongs on launch day (§13). Setting it early makes the pages.dev preview redirect to a domain that isn't serving the new site yet, which takes away the URL you were going to review on.

The next section does all of this in one command instead, if you'd rather.

#6. Configure Cloudflare in one command

Paste this to do it with Claude:

Configure the Cloudflare Pages project for <domain>. Run migration-kit/setup-client.mjs
with --dry-run first, show me the output, and only run it for real once I say so.
Then check every environment variable from the table in the onboarding doc is
actually set, and tell me which are missing. Do not put the API token on the
command line — read it from migration-kit/.env.

This is a Terminal command, not something you type into the dashboard or a chat window.

One-time setup. Create a Cloudflare API token — Cloudflare → My Profile → API Tokens → Create Token → Custom token, with these three permissions:

Your Account ID is in the right-hand sidebar of Workers & Pages. Put both in migration-kit/.env (copy .env.example). That file is gitignored and stays on your machine.

Then, for each client: open Terminal, go to the workspace folder, and run:

cd "/path/to/M1 TEst/migration-kit"
node setup-client.mjs \
  --project <pages-project-name> \
  --domain <client-domain.co.uk> \
  --site-id <PUBLIC_SITE_ID> --key <PUBLIC_SITE_INGEST_KEY> \
  --ga4 G-XXXXXXX \
  --dry-run

The \ at the end of each line just means "this continues below" — paste the whole block at once. Drop --ga4 if there's no Analytics property yet.

Run it with --dry-run first. It prints every call it would make and changes nothing. When the output looks right, run the same command again without that flag.

#Don't put the token in the command

You may see the older form with CF_API_TOKEN=xxx at the front. It still works, but a secret typed into a command is not private: your shell saves it to ~/.zsh_history, any process on the machine can read it out of ps, and if you run the command through an assistant it ends up in the transcript. Keep it in .env and run the plain command.

#What it does

Sets all production env vars, adds the apex custom domain, turns off Email Address Obfuscation, and triggers a rebuild. Then it prints the three steps that genuinely can't be scripted — deploy hook, www→apex redirect, and Search Console.

Two choices in it are deliberate:

#The environment variables it sets

Env vars in Cloudflare are the ones that count. .env in the site repo is local only and gitignored — the "it worked locally" failure has bitten twice, both times GA4 and consent.

You rarely need to set these by hand, but you do need to be able to check them. Pages project → Settings → Environment variables (Production):

Variable Value What breaks without it
PUBLIC_SITE_ID from /sites/<id>/setup The site can't fetch its content or report conversions.
PUBLIC_SITE_INGEST_KEY from the same page Conversions are rejected — leads still save, attribution doesn't.
PUBLIC_PLATFORM_URL https://majestic-system-theone.vercel.app Content fetch fails; pages fall back to built-in defaults.
CONTENT_API_URL same value Older name for the same thing; some renderers still read it.
SITE_URL https://<domain> robots.txt and the sitemap advertise the wrong domain — this is how eleven sites once pointed at Ferndown's sitemap.
CANONICAL_HOST <domain> functions/_middleware.js stops redirecting *.pages.dev → the real domain, so the preview URL gets indexed as duplicate content.
PUBLIC_GA4_ID G-XXXXXXXXXX No analytics at all. Silent — the site looks perfect.
PUBLIC_CONSENT_REQUIRED true See below.
NODE_VERSION 20 Build runs on Cloudflare's default Node and may fail or behave oddly.

PUBLIC_CONSENT_REQUIRED stays true. The script defaults it to true; --no-consent sets it to false and makes GA4 load before the visitor has chosen. Under UK GDPR/PECR, analytics cookies are non-essential and need consent first, and a banner that appears after tracking has started is not consent. There is no client on this estate where false is the right answer.

Anything PUBLIC_* is inlined into the browser bundle at build time and is meant to be visible — a GA4 measurement id and the ingest key are both readable in page source on every site that has them. They are not secrets. Nothing that is a real secret belongs in a client site's environment at all.

Changing an env var does not rebuild the site. Set it, then trigger a deploy — the deploy hook, or a push. This is the same trap as rotating CRON_SECRET in Vercel.

#Before the DNS switch — the handover pack

Do not start section 7 until the client has been sent all three of these links. The DNS switch is the point of no return; the handover pack is what earns the client's sign-off to press that button — and it is far more persuasive sent before launch ("here is your new site, ready to go") than after.

Every handover pack is the same three links:

  1. The live website — the <project>.pages.dev preview. It serves the whole site with an X-Robots-Tag: noindex header until CANONICAL_HOST is set at launch (the middleware handles this), so it cannot compete with the client's current site in Google. After launch it 301s to the real domain, so the link never goes stale. Warn the client that forms and calculators on the preview deliver real leads to the inbox.

  2. The report of what was done — a design-overview page at reports.majesticone.co.uk/<client>/design/, built from the repo's migration/IMPROVEMENTS.md. Real screenshots of every page type, the design system, the before/after performance table, and the honest audit list (what was broken, what we did). Every figure in it must be measured, not estimated — capture the "before" numbers while the old site is still up.

  3. The brand guidelines — reports.majesticone.co.uk/<client>-brand/, per the pattern in the share repo's README (self-contained index.html + a logo/ folder of downloadable files). Set brandGuidelinesUrl on the site record so it also appears in the client's portal.

Both report pages live in the majestic-one-share repo — publishing is adding a folder and pushing to main. The _headers file already sends noindex on everything there.

Reference example (BCC, August 2026): thebigconversioncompany.pages.dev · reports.majesticone.co.uk/thebigconversioncompany/design/ · reports.majesticone.co.uk/thebigconversioncompany-brand/

#After acceptance — archive the old site, then hand the client a copy

The client has said yes. The next thing that happens is not the DNS switch — it is the funeral arrangements for the old site. Once the domain moves, anything you didn't capture is gone, and some of it (form enquiries, plugin configurations) exists nowhere else.

Capture all of it, in this order of value:

  1. Hosting-panel full backup — the site files and the database, from the host's own backup tool (Hostinger, cPanel, etc.). This is the only copy that contains the WordPress database: posts, pages, users, settings.
  2. Form enquiries — plugin-stored submissions (Forminator, CF7 databases, Gravity Forms entries) are in the database, but export them separately as CSV from wp-admin too, so the client can open them without restoring WordPress. These are the client's sales records.
  3. Plugin inventory and configurations — a list of active plugins and versions, and exports of any plugin whose configuration is business logic (calculator formulas, redirect maps, SEO settings). On BCC the calculator formulas lived only inside Forminator's config.
  4. Static mirror — every page as served, plus assets and sitemaps, taken with a crawler while the site is still up. This is the fallback that needs no hosting access, and the reference for "what did the old page actually say". (On BCC: site-backups/<domain>-<date>/ + zip, with a README stating what is and isn't inside.)
  5. DNS zone snapshot — every record, especially MX/TXT/DKIM. Section 6 requires this anyway; capture it here so the archive is complete.

Then deliver a copy to the client (WeTransfer or similar for the multi-GB zip) and keep a copy our end, stored with the client's files. Tell the client in writing what the archive contains and what it doesn't — a static mirror is not a database backup, and saying so now beats explaining it in a dispute later. The client owning a full copy of their old site is part of the handover, not a courtesy: it's their content, their enquiry history, and their rollback insurance.

Only when the archive exists in two places (theirs and ours) does the switchover begin.

#7. DNS and the custom domain

Paste this to do it with Claude:

I'm doing the DNS cutover for <domain>, following section 7 of the onboarding doc.
Before I touch anything: capture the current zone — every MX, TXT, DKIM, DMARC and
subdomain record — and write it to <repo>/docs/dns-cutover.md as a rollback
reference. Tell me who the registrar is and who hosts the DNS, and whether the
client has live email on this domain. Then walk me through the phases one at a
time, checking each one before we move on. I will do the nameserver change myself.

Claude can read DNS, verify each phase and tell you what is wrong. It cannot log in to the registrar — phase 5 is yours.

This is the only step you cannot roll back. A website reverts in two minutes. A client's lost email does not. Everything before phase 5 is reversible; phase 5 moves live traffic and live mail at the same moment.

Work through the phases in order. Each one says what it risks, so you know when you are still safe and when you are committed.

#Before you start: capture the existing zone

Write down every record that exists today, before you change anything. That list is your rollback. fra-dorset-site/docs/dns-cutover.md is the worked example — copy its format.

Two facts to establish first, because both are commonly assumed wrong:

#Phase 1 — Add the zone (no risk, nothing changes yet)

  1. Cloudflare dashboard → Add a site → type the domain
  2. Choose the Free plan
  3. Cloudflare scans the current host and imports what it can find
  4. It shows two nameservers (something.ns.cloudflare.com) — write these down, you need them in phase 5

Nothing is live. The old nameservers are still authoritative until phase 5, so you can edit freely here without affecting anyone.

#Phase 2 — Audit the imported records (no risk)

DNS → Records. Check every record from your capture exists. The scan is good, not perfect, and a record it missed only hurts after phase 5 — by which point mail is already down.

Check these especially:

Type Typical name Why it matters
MX @ Mail delivery. Note the priorities.
TXT @ SPF (v=spf1 …). Missing → outbound mail lands in spam.
TXT @ Domain verification (google-site-verification=…). Missing → Search Console de-verifies mid-migration, exactly when you need the data.
TXT _dmarc DMARC policy.
CNAME *._domainkey DKIM, usually three of them. Missing → mail fails signature checks.
CNAME autodiscover, autoconfig Outlook/Apple Mail auto-setup.
A / CNAME anything else Subdomains you already run — a client portal, a docs site, an FTP host. These matter as much as email and are the easiest to forget, because nobody thinks of them as "the website".

Set every mail record to DNS-only (grey cloud). Cloudflare's scan imports records proxied by default, and a proxied record answers with Cloudflare's own IPs instead of the real target. The proxy only handles HTTP/HTTPS, so for mail this breaks things without breaking them loudly: DKIM stops verifying and mail scores as spam rather than bouncing. MX and TXT cannot be proxied, so it is only ever the CNAMEs.

#Phase 3 — Remove the old site's records (no risk yet)

Delete the records pointing at the old host — usually the apex A records and the www CNAME. Phase 4 replaces them.

Still safe: the old nameservers are authoritative, so nothing you do here is visible yet.

#Phase 4 — Attach the domain to Pages (no risk yet)

  1. Workers & Pages → the project → Custom domains → Set up a custom domain
  2. Enter the apex (client.co.uk). Cloudflare sees the zone is in your account and creates the record itself.
  3. Repeat for www.client.co.uk

Then SSL/TLS → Overview → confirm the mode is Full (strict). If it is on Flexible you get an infinite redirect loop the moment DNS switches. Thirty seconds now, or a broken site and a confusing hour later.

#Phase 5 — Change the nameservers (irreversible — this moves everything)

Baseline first: send a test email to the client's address and confirm it arrives. If mail is broken after the switch you need to know whether you broke it.

  1. Log in to the registrar — not the hosting panel
  2. Manage the domain → Nameservers
  3. Replace the existing pair with the two Cloudflare gave you in phase 1
  4. Save
  5. Check the registrar has no web forwarding or URL redirect enabled on the domain. It overrides DNS and will hijack the site no matter what Cloudflare says.

Propagation is usually 15–60 minutes.

#Phase 6 — Verify

d=client.co.uk
for r in NS A MX TXT; do echo "== $r =="; dig +short $r $d; done
echo "== site =="; curl -s -o /dev/null -w '%{http_code}\n' https://$d
echo "== www redirects to apex =="; curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://www.$d/

Expect: Cloudflare nameservers, the MX and TXT records unchanged from your capture, 200 on the apex, and 301 from www to the apex.

Then send a test email in and out of the client's address. Both directions — inbound proves MX, outbound proves SPF and DKIM.

Finally, check a handful of URLs that had traffic still return 200 at their original paths. Nothing that ranked may 404.

#Phase 7 — Lock in the canonical host

  1. Pages project → Settings → Variables → CANONICAL_HOST = the domain
  2. Deployments → latest → Retry deployment — env var changes do not rebuild on their own

This makes <project>.pages.dev 301 to the real domain, so Google cannot index the preview URL as duplicate content.

Add the www → apex 301 as a Redirect Rule (wildcard pattern https://www.* → https://${1}, status 301). The rule only fires on proxied traffic, so if www is grey-clouded it will silently do nothing.

#Phase 8 — Clean up

#If it goes wrong after phase 5

Remove the custom domain from the Pages project and restore the old A records in Cloudflare. The site reverts in minutes and the zone keeps every mail record. You do not need to move the nameservers back.

#What Cloudflare switches on by itself

Adding a zone enables two AI-related settings without asking. Neither breaks anything, but the first time you read a client's robots.txt and find rules nobody wrote, this is why.

Your robots.txt will not match your repo. src/pages/robots.txt.ts generates four lines; what gets served has a Cloudflare "Managed content" block prepended — content signals, plus Disallow: / for around nine AI crawlers. Your own rules still appear underneath and your Sitemap: line survives. Nothing is wrong.

Search engines are unaffected. Googlebot, BingBot and Baidu are all allowed. There is no Disallow: / for the generic User-agent: *. Blocking Google would be catastrophic and is not what this does — check for it anyway if a site's traffic ever falls off a cliff.

The split is between crawlers that answer questions and crawlers that harvest for training:

Status Examples
Search engines allowed Googlebot, BingBot, Baidu
AI search & assistants allowed Claude-SearchBot, OAI-SearchBot, PerplexityBot, ChatGPT-User, Perplexity-User, DuckAssistBot, MistralAI-User, Applebot
AI training crawlers blocked GPTBot, ClaudeBot, CCBot, Bytespider, Amazonbot, FacebookBot, Google-CloudVertexBot, Meta-ExternalAgent

That is the right way round for our clients. When someone asks an assistant "who does X near me", the bot that fetches the site is a search bot, and those are allowed — which is what the platform's AI Visibility page is measuring. What is blocked is bulk ingestion into training sets, which does nothing for the client either way.

The per-crawler list is read-only while this is on. AI Crawl Control → Security shows 32 crawlers with individual Block switches, but they are overridden by the zone-level Block AI Bots setting — clicking one just shows a tooltip saying so. To control a single crawler you must disable Block AI Bots first, which unblocks all fifteen at once, and then re-block the others by hand. On a twelve-site estate that is roughly 170 toggles and a window on each zone where nothing is blocked.

So leave it alone unless a client specifically asks to allow AI training use of their content. The gap it leaves — Claude-User, the fetch when a Claude user asks about a page — is not worth dismantling a working default for, because every other assistant's equivalent is already allowed.

#8. Deploy hook

Cloudflare → the project → Settings → Builds & deployments → Deploy hooks → create. Paste the bare URL into the platform's Publishing box on the site record.

This is what lets the platform rebuild the site after it publishes content. Without it, approved changes sit in the platform and never appear.

#9. Analytics and Search Console

Both halves are required. Without the platform's Google connection, every SEO figure in the client's reports stays at zero. Expect GSC data to lag days to weeks on a new property — leads and conversions are the only instant numbers.

#10. Verify — don't sign it off until these pass

Paste this to do it with Claude:

Verify the build and deployment for <domain>. Run npm run build and the analytics
verifier, then check from the live site: every URL in the sitemap returns 200, an
unknown path returns 404, the pages.dev URL 301s to the real domain, www redirects
to the apex with the path preserved, and robots.txt points at the right sitemap.
Report what actually happened, including anything that failed.

Every check below has an explicit pass condition. "It looked fine" is not one of them: most of the failures this list exists to catch look completely fine.

Run them in this order. The automated ones are cheap and rule out whole classes of problem before you spend time clicking.

#Check 1 — the build guard

npm run build

Pass: verify-build reports clean and the page count matches what you expect.

It blocks on broken internal links, blank pages and cross-domain contamination. If it fails, it is almost certainly right — it once caught every site in the estate advertising the wrong client's sitemap. A page count that dropped is as much a failure as an error: it means pages stopped generating and nothing said so.

#Check 2 — analytics actually send

node migration-kit/verify-analytics.mjs https://<domain> --id G-XXXXXXX

Pass: it reports the hit fired.

This matters far more than it sounds. It loads the page in a real browser and asserts the GA request is actually sent — a tag can be present, load cleanly, fill dataLayer and never send anything. That happened to every site at once in June 2026 and Tag Assistant showed nothing wrong. On a consent-gated site you have to accept cookies for this to pass, which is correct.

#Check 3 — a real lead reaches a human

Submit the contact form on the live site, as a visitor would.

Pass: it arrives in both the dashboard inbox and the notify email inbox.

Both, not either. The dashboard alone means the client never hears about it. This is the single most expensive thing to get wrong — on a fire-safety or emergency trade, a swallowed enquiry is someone who needed help and got silence.

#Check 4 — the live domain serves the new site

View source on the live domain.

Pass: you can see /_astro/ asset paths, and none of the old site's (/wp-content/).

DNS and caching both lie about this. Looking at the HTML is the only reliable answer.

#Check 5 — nothing that ranked has 404'd

Migrations only, and this is the one that costs real money.

Take the URLs from the client's Search Console export — every page with impressions, not a sample.

Pass: every one returns 200 at its original URL, or 301 to a sensible equivalent. Any 404 is a fail, however unimportant the page looks.

#Check 6 — look at it

Open the site in a browser. Phone and desktop. Click the nav, submit the form, load a few deep pages.

Pass: nothing is visually broken and every interactive thing does what it says.

This is not padding. Several of the worst bugs this project has shipped were invisible in code and obvious on screen: white text on a white background, images at the wrong aspect ratio, a booking form with no handler that silently did nothing. No automated check catches those.

#Check 7 — the platform knows about the site

Open /sites/<id>/setup and read the go-live checklist.

Pass: nothing is marked blocking.

It reads the site's real configuration rather than a list someone remembered to update. Section 10 covers what each item means and why it fails silently.

#11. Configure the platform — the part that fails silently

Paste this to do it with Claude:

Open the go-live checklist at /sites/<id>/setup for <domain> and tell me what is
still missing. Then set what you can from evidence that already exists — service
areas and tracked areas from the site's own area pages, the Google Business Profile
from the dropdown. Ask me about anything that needs a judgement call, especially
target keywords and competitors. Flag it if the tracking cost goes above the
standard package.

Everything above is infrastructure: if you get it wrong, something visibly breaks. This section is the opposite. Leave any of it blank and nothing errors — no page looks wrong, no build fails. It surfaces weeks later as a lead nobody answered, or a suggestion to build a town page that arrives filed as a blog post.

Leave "This site is live" switched off until launch day. It's the first panel on the setup page. The daily self-test judges a site on whether its Search Console and Business Profile data is genuinely arriving — and a site that hasn't launched has none, by definition. Left ticked during a build, it fails the check every morning until people stop reading the email, which is the one thing that alert cannot afford. Turn it on once the domain is switched over. §13 covers that.

Work through the go-live checklist on /sites/<id>/setup. It reads the site's real configuration and lists what's missing, marks which items would break something, and links straight to the page that fixes each one. Don't sign a site off with anything still marked blocking.

What it covers and why each one matters:

Set this Where If you skip it
Lead notification email /leads The form submits fine and nobody is told. Worst failure available.
Deploy hook /setup Approved changes sit in the database and never reach the live site.
Pages imported /pages The platform can't see the site's structure and invents target URLs.
Search Console /setup No query data at all — every recommendation is guesswork.
Analytics /setup Nothing to measure a change against.
Service areas /intel Location content isn't recognised: "build a Bournemouth page" arrives as a blog post.
Tracked areas /seo Rankings measured nationally only — a page at #2 in its town reads as #40.
Target keywords /seo No way to tell whether anything worked.
Competitors /battle-plan No battle plan, no share of voice.
Google Business Profile /setup No map pack. For a local trade that's usually where the calls come from.

The two "areas" fields are different things and you need both. Tracked areas (/seo) drive rank tracking per town. Service areas (/intel) tell the AI which places this client actually covers. They are set in different screens and neither is required, which is how dorsetheatpumpinstaller.co.uk ended up with neither — its Bournemouth opportunity, 1,095 impressions sitting at position 43, was queued as a blog post for months because nothing knew Bournemouth was one of its towns.

#12. Brand guidelines

Every client gets a brand guidelines page — logo, colour, typography, voice, and downloadable logo files. FRA Dorset's is the reference: majestic-one-share/fra-dorset-brand/.

These live in majestic-one-share, not on the client's own website. Hosting an agency deliverable on the client's production site means it competes for crawl budget, relies on a meta tag to stay out of Google, and has to be special-cased in verify-build and the sitemap. In majestic-one-share, _headers sends noindex, nofollow as a real HTTP header on every path — which covers the logo files too, not just the HTML.

majestic-one-share/<client>-brand/
  index.html          <- the page
  logo/               <- svg + png, on-light / on-dark / transparent / mark

Asset paths inside the page must be relative (logo/x.svg), so it works at whatever slug it lands on. Commit, push, and it deploys.

Then set brandGuidelinesUrl on the site record — on the site overview page, /sites/<id>, not the setup wizard — so it appears in the client's portal.

The link is unlisted, not private: anyone who has it can open it. That is fine for a client's own logo and colours. Don't put anything in there you wouldn't want forwarded.


#13. Launch day — the full sweep

Paste this to do it with Claude:

<domain> has just gone live. Work through the launch-day sweep in section 13 of the
onboarding doc and report what passes and what doesn't. Check every page renders in
a browser at desktop, tablet and mobile — actually look, don't just read the markup.
Verify the email address in the footer resolves to a real mailbox. I'll submit the
test lead and the sitemap myself; tell me when to.

Do this after the domain is serving the new site, in one sitting. Everything here has been missed at least once on this estate, and every one of them fails quietly.

Start by ticking "This site is live" on /sites/<id>/setup. Until you do, the site is excluded from the daily self-test — which was right while it was being built and is wrong the moment it is serving real visitors. From that tick onwards you get told each morning if its data stops arriving, if the pages.dev copy isn't redirecting, or if the live site is telling Google not to index it.

Also confirm CANONICAL_HOST is set on the Pages project. Without it the pages.dev host keeps serving a full copy of the site instead of redirecting to the real domain. The middleware noindexes it either way, so nothing is indexed — but it should be a redirect once you are live, and the self-test will flag it the next morning if it isn't.

Work top to bottom. If something fails, fix it before moving on — a broken form matters more than a sitemap.

#Does it work?

#Does it look right?

#Will Google index it correctly?

#Is it being measured?

#Then tell the client


#14. The follow-up, three to seven days later

Paste this to do it with Claude:

<domain> launched <date>. Do the follow-up checks from section 14: what does Search
Console say about indexing and the sitemap, has a real lead arrived, is Analytics
recording sessions from more than just us, and on a migration has anything that
ranked before dropped? Compare against the pre-launch export where there is one.
Tell me plainly if something looks wrong.

Book it in when you launch, because the failures below cannot be seen on launch day. They need Google to have crawled, or a real member of the public to have used the site.

If all of that is clean at a week, the launch is done.

#Known friction — expect these

#Standard defaults for every client

Email obfuscation off · www → apex 301 · *.pages.dev → domain 301 (handled in the renderer) · preserve MX/TXT on any DNS move · NODE_VERSION=20 · build npm run build · output dist.

Internal documentation. Not for clients, and not for search engines — this page sends noindex, but the link itself is the only thing keeping it private.