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
- Never build from scratch. The platform writes content into these sites, and every section type it can send must exist in the renderer or the page renders blank. Restyle the template freely; don't replace its plumbing.
- Never invent a trust claim — reviews, ratings, accreditations, certifications, insurance, memberships, years in business, project counts, case studies. All of it comes from the client. If you need a number and don't have it, ask.
- Structural things stay in the template — icons, slugs, image paths, schema, robots and sitemap URLs. Anything sourced from editable page data can be wiped by a content rewrite.
#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.
- Build command:
npm run build - Output directory:
dist - Environment variable:
NODE_VERSION=20
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-ferndowndoes), so Cloudflare's default decides it unless you setNODE_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:
- Account · Cloudflare Pages · Edit
- Zone · Zone Settings · Edit
- Zone · DNS · Read
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:
- Apex domain only.
wwwmust redirect, not serve a second copy — serving both created duplicate content on the Dorset site. - Email obfuscation off. Left on, Cloudflare rewrites addresses to
cdn-cgi/email-protectionand Ahrefs reports a 404 on every page.
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:
- Add the domain to Cloudflare. It scans and imports the existing records.
- 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.
- Change the nameservers at the registrar.
- Wait for the zone to go active. The old site is still serving at this point — nothing has changed for visitors.
- 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.
- Add the custom domain to the Pages project. This is the moment the new site goes live.
- 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
- Set
PUBLIC_GA4_ID(and the consent choice) — in Cloudflare, not just.env. - Google Search Console → add a Domain property → verify with a TXT record in Cloudflare DNS
→ submit the sitemap (
sitemap-index.xmlon Astro sites). - Platform →
/sites/<id>/setup→ Connect Google, then set the GSC site URL, the GA4 property id and the Business Profile location.
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:
- Submit a real test lead and confirm it reaches the dashboard inbox and the notify email.
- Check the live domain serves the new site — view source and look for
/_astro/rather than the old site's asset paths. - On a migration, check the old URLs. Every page that had traffic must return 200 at its original URL, or 301 somewhere sensible. Nothing that ranked may 404.
- Open it in a browser. Several of the worst bugs here were invisible in code and obvious on screen: white text on white, an image at the wrong aspect ratio, a form that silently did nothing.
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
- Cloudflare "Name already in use" when adding an env var → you must edit, not add.
- GitHub Desktop "no files to commit" → it already auto-committed; just push.
- There is still no "delete page" in the platform. Removing a page needs SQL. Ask before doing it.
- GA Realtime lags on a brand-new property, and ad blockers produce false alarms. Use
verify-analytics.mjsrather than guessing.
#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.