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.

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


#1. Create the client in the platform

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.

#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 → PagesConnect 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.

The repos don't pin a Node version (only electrician-ferndown does), so Cloudflare's default decides it unless you set NODE_VERSION. Astro 5.6 needs 18.17+. Set it explicitly.

The first build may fail until the env vars exist. That's expected — the next step fixes it.

#5. Configure Cloudflare in one command

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:

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.

#6. DNS and the custom domain

This is the step that can break something you cannot roll back. A website reverts in two minutes. A client's lost email does not.

Before touching anything, write down the existing zone — every MX, TXT (SPF), DKIM and DMARC record. fra-dorset-site/docs/dns-cutover.md is the worked example; copy its format.

Then, in this order:

  1. Add the domain to Cloudflare. It scans and imports the existing records.
  2. Check every mail record imported. MX, SPF, DKIM (often three CNAMEs), DMARC, plus any domain-verification TXT. Add anything missing by hand. The scan is good, not perfect.
  3. Change the nameservers at the registrar.
  4. Wait for the zone to go active. The old site is still serving at this point — nothing has changed for visitors.
  5. Send a test email to and from the client's address. Do this before step 6. If mail is broken, fix it before touching the website.
  6. Add the custom domain to the Pages project. This is the moment the new site goes live.
  7. Add the www → apex 301 (Redirect Rule, wildcard pattern).

If anything looks wrong after step 6, remove the custom domain from the Pages project and restore the old A records. The site reverts; the zone keeps the mail records.

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

#8. 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.

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

npm run build                                   # verify-build must pass
node migration-kit/verify-analytics.mjs https://<domain> --id G-XXXXXXX

Then by hand:

verify-analytics.mjs matters more than it sounds — it loads the page in a real browser and asserts the GA hit actually sends. A tag can be present and never fire; that happened to every site at once in June 2026, and Tag Assistant showed nothing wrong.

#10. Brand guidelines

Every client gets a brand guidelines page — logo, colour, typography, voice, and downloadable logo files. FRA Dorset's is the reference. Host it on the client's Pages project at /brand/, then set brandGuidelinesUrl on the site record so it appears in the client's portal.


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