Onboarding a developer

Follow the phases in order. Each one finishes with a check that tells you it worked, so you never move on wondering.

This document is about getting you working, not about building any particular site. The process for building and launching a client website is a separate document — Onboarding a client — because it's the same whoever runs it.

Every rule in here exists because breaking it has already cost us something real.

#How to use this document

Every phase 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 for the phase you're on. Claude does the work and asks you to approve each action.

By hand. Ignore the grey blocks and follow the numbered steps. They're complete on their own — every command and click path is written out.

The grey blocks are self-contained, so you can paste them out of order.

What Claude cannot do, and will say so:


#Phase 0 — Accounts

Nothing else works until these are done, and the first one is yours to do.

#0.1 Create your GitHub account — do this first

You need your own account; nobody can make one for you.

  1. Go to github.com/signup.
  2. Use an email you'll keep. A personal address is fine.
  3. Pick a username — it appears on every commit you make, so keep it professional.
  4. Verify the email GitHub sends you. An unverified account can't be added to a repo.
  5. Turn on two-factor authentication: Settings → Password and authentication → Two-factor authentication. GitHub requires it for contributors, and you'll be locked out mid-job if you leave it until it's forced.
  6. Send Oliver your GitHub username — not your email, the username. That's what he needs to invite you.

#0.2 What Oliver then sets up

Chase these if they don't appear; you can't do any of them yourself.

You do not need a Cloudflare account — see the section near the end. Deploys happen automatically when you push, with no login involved.

#Which repos you get

Repo Why Needed
<client>-site — e.g. thebigconversioncompany-site The job. Created from client-template, so it already contains CLAUDE.md, BUILD-BRIEF.md and the README — you don't need separate access for those. Always
migration-kit verify-analytics.mjs (proves the GA4 hit fires) and the prerender tools for migrating a React/SPA site. When the job needs it
client-template Only if you're improving the template itself. Your client repo is already a copy of it. Rarely

You do not get seo-platform. It holds the platform's production configuration and nothing you need to build a client site.

The manuals — including this page — are a public website at docs.majesticone.co.uk. No repo access needed to read them.

#Oliver's side — the whole list

Once the developer sends their GitHub username:

  1. Create the client repo — client-template on GitHub → Use this template → name it <client>-site, set Private. It arrives carrying CLAUDE.md, BUILD-BRIEF.md and the README already.
  2. Invite them — that repo → Settings → Collaborators → Add people → their username.
  3. Create their platform login — portal.majesticone.co.uk → Team → Add someone. Leave "Make them an admin" unticked. Hand over the password in person or via a password manager.
  4. Decide how main is protected before the site goes live. A collaborator has push access, and a push to main is a deploy. Note that branch protection is not available on private repos on the free plan — GitHub returns "Upgrade to GitHub Pro or make this repository public" — so until you upgrade, "don't push to main" is a convention rather than something enforced. With Pro or an Organisation, require a pull request stops accidental direct pushes while still letting the developer merge their own work; adding require approvals is what puts you in the loop on every deploy. They are separate settings and only the second one makes you a bottleneck.
  5. Fill in BUILD-BRIEF.md and commit it, or send it over.
  6. Share the Search Console export if the client already has a site.

Notes for Oliver. Three things about a personal GitHub account, as opposed to an Organisation:

  • Collaborators are invited per repository — there's no team to add someone to once.
  • Collaborators cannot create repositories. You create each client repo from client-template (tick Template repository in its settings once, then "Use this template"), then add the developer to it.
  • There is no read-only collaborator role. On a personal repo, a collaborator has push access. If you want genuine read-only or per-repo permission levels, that needs an Organisation. Bear that in mind before adding anyone to a repo that deploys somewhere live.

#Phase 1 — Install the tools

You need four things: Node, Git, the GitHub CLI, and Claude Code.

Paste this to do it with Claude:

I'm a new developer setting up my machine for the Majestic One workspace.
Check whether Node (v20 or newer), git, and the GitHub CLI (gh) are installed,
tell me the versions, and give me the exact install commands for anything
that's missing on my OS. Don't install anything without asking me first.

By hand. Check what you already have:

node --version && git --version && gh --version

Anything missing, on macOS:

brew install node git gh

Check it worked: all three commands print a version, and Node is v20 or newer.


#Phase 2 — Connect to GitHub

This is where setup usually goes wrong, so do it exactly as written. Never paste a token into a terminal — the browser flow below doesn't need one, and a token typed on a command line is saved to your shell history where any process can read it.

1. Accept the invite. Check your email for "invited you to collaborate" and click the accept link. Do this first — the commands below fail confusingly without it.

2. Log in. Run this and follow the browser prompts:

gh auth login --hostname github.com --git-protocol https --web

Choose GitHub.com, then HTTPS, then let it authenticate in the browser. It stores the credential itself, so git will stop asking.

3. Prove it worked. This must print your username and show the repos you can reach:

gh auth status && gh repo list majesticonemain-a11y --limit 30

If the repo list is empty, the invite hasn't been accepted yet. Go back to step 1.

4. Set your commit identity — replace both values with your own:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

Check it worked: gh auth status says Logged in to github.com, and your client's repo appears in the list.


#Phase 3 — Get the code

The repo is the website. Nobody uploads a site: you push code, Cloudflare builds it. Your local folder is a disposable working copy, so push at the end of every session — anything unpushed exists only on your laptop.

#Which repo?

One repo per client website, named after the client, always ending -site. Not after the domain — tjcelectrical.co.uk lives in tjc-electrical-site.

To see every repo you have access to:

gh repo list majesticonemain-a11y --limit 50

The ones you'll be working in:

Client Repo
The Big Conversion Company thebigconversioncompany-site
TJC Fire & Electrical tjc-electrical-site
The Green Home Co thegreenhomeco-site
Dorset Damp Specialist dorset-damp-specialist-site
French Drain Installation french-drain-installation-site
Soakaway Installation soakaway-installation-site

If the list comes back empty or short, you haven't accepted all the invites yet — go back to Phase 2 step 1.

Ask Oliver which client to start on, and whether it's a rebuild of an existing site or a fresh build. That decides everything in Phase 5, and it isn't guessable from the repo.

#Clone it

Substitute the repo name from the table above — the example uses the first one:

gh repo clone majesticonemain-a11y/thebigconversioncompany-site
cd thebigconversioncompany-site
npm install

Then create your environment file. .env is gitignored and never comes from a clone:

cp .env.example .env

Two of the values are per site, one is the same everywhere:

Value Where from Changes per site?
PUBLIC_SITE_ID portal.majesticone.co.uk → the site → Setup Yes
PUBLIC_SITE_INGEST_KEY same page Yes
PLATFORM_IMPORT_SECRET Oliver, once — via a password manager No — identical for every site

So you only ever ask for the import secret once. On the second and third sites, copy it from the .env you already have. You're retyping it per repo purely because .env is gitignored and doesn't survive a clone.

It unlocks the content import and nothing else, so it's safe to keep in your local .env files — but it doesn't go in a commit, a chat or a command line.

Start it up:

npm run dev

Check it worked: the site loads at the address printed in your terminal.

#If the import returns 401

Almost always one of three things, in this order. Do not ask for a new secret first — it is rotated across 21 sites, so replacing it is expensive and is rarely the answer.

1. A stale value is exported in your shell. This is the one that has cost a day before. The .env loader deliberately skips any key already present in the environment:

if (!m || m[1] in process.env) continue;

So an exported PLATFORM_IMPORT_SECRET silently beats the file, and the import keeps returning 401 no matter what you paste into .env. Check and clear it:

echo "$PLATFORM_IMPORT_SECRET"   # anything printed here is overriding your .env
unset PLATFORM_IMPORT_SECRET

Also check your shell profile (~/.zshrc, ~/.bash_profile) in case it is exported there on every new terminal.

2. You ran it from the wrong directory. The loader reads .env relative to the current working directory, so it must be run from the repo root.

3. The value itself is wrong. Check it without revealing it — this prints only a short fingerprint, which is safe to paste into a message:

sed -n 's/^PLATFORM_IMPORT_SECRET=//p' .env | tr -d "\"' \r\n" | shasum | cut -c1-8

Send that to Oliver, who can compare it against the current value. A mismatch means you have an old or mistyped copy; a match means the secret is fine and it is one of the two causes above.

#It is not CRON_SECRET

It used to be, and some older notes still say so. They are separate values now. This matters because it points at the wrong fix: rotating CRON_SECRET does nothing for a failing import, and rotating the import secret breaks all 21 sites until every copy is updated.

If you were given a value that is not the import secret, it may still work today — the import endpoint accepts either — and then fail later when the other one is rotated, which looks exactly like a stale import secret. The fingerprint check above is what tells them apart.


#Phase 4 — Understand what you're building

#What this business actually does

Most small firms pay an agency for SEO every month and never see it working — no data, no proof, no idea what changed. Majestic One is the platform that fixes that. It watches a client's Search Console, Analytics, Business Profile and rankings every day, works out the highest-impact change, and — once a person has approved it — makes that change on the client's live website and measures whether it worked.

Two consequences shape everything you'll do:

  1. We own the client's website. We don't bolt a plugin onto their WordPress; we rebuild it as clean static code the platform can write into. That's why the sites are ours to build and why they all share one contract.
  2. Everything must be provable. Figures come from Google and the client's own site. We never estimate and present it as fact, and we never invent anything.

#The map

What Where it lives Deploys to
The platform (dashboard, AI, ingestion) seo-platform Vercel → portal.majesticone.co.uk
Client websites (~13) one repo each, e.g. hal-construction-site Cloudflare Pages → the client's domain
Starting point for a new client site client-template —
Tools for migrating an existing SPA migration-kit —
Internal manuals — including this one majestic-one-instructions docs.majesticone.co.uk

Client sites are Astro. The platform is Next.js.

#The section contract — the most expensive thing to get wrong

The platform writes content into these sites as typed sections. src/components/SectionRenderer.astro maps each type to a component and ends with default: return null.

A type the renderer doesn't know renders nothing. The page is blank — not broken, blank. Nothing errors, nothing logs, and it stays that way until a client notices.

These nine types must always exist and render:

homeTemplate · servicesIndex · serviceTemplate · areasIndex · areaTemplate · aboutTemplate · contactTemplate · richText · blogPost

This is why you never build a client site from scratch. client-template satisfies the contract; a bespoke site built from nothing does not. Restyle the template as much as you like — don't replace its plumbing (src/lib/platform/*, functions/, SectionRenderer).

The repo's own CLAUDE.md carries these rules into your Claude Code session automatically.


#Phase 5 — How your work gets reviewed

Push a branch. Don't send a folder.

It's tempting on the first couple of sites to zip the folder and email it. Don't — a branch is safer and easier to review:

npm run build
git push -u origin descriptive-branch-name
gh pr create --fill

Send Oliver the preview URL. The rendered page is the review, not the diff.

Never push straight to main on a client site. A push to main is a deploy.

#The two preview URLs, and which one you're sending

Cloudflare gives every project a pages.dev host, and there are two kinds:

URL What it is When you use it
<branch>.<project>.pages.dev Your branch, built on its own Reviewing a change to a site that's already live
<project>.pages.dev Whatever's on main Reviewing a whole new site before its domain is connected

The second one is the important one for a new build. Before the client's domain is pointed at Cloudflare, <project>.pages.dev is the site — a complete, working copy on a URL that isn't the client's. That's what Oliver signs off before anything touches the real domain.

Both are noindex. They're near-complete copies of a client's site, and an indexed one competes with the real domain for the client's own search terms. functions/_middleware.js sets X-Robots-Tag: noindex, nofollow, noarchive on every pages.dev response until the site launches, and after launch redirects the production preview to the live domain while leaving branch previews working so reviews still happen.

You can confirm it any time:

curl -sI https://<project>.pages.dev/ | grep -i x-robots-tag

If that prints nothing on a preview host, stop and say so — it means a copy of the client's site is open to Google. This exact thing happened on FRA before anyone noticed.

Check it worked: the PR shows a Cloudflare preview link and the build is green.


#Building and launching a site

That process is not in this document. It lives in Onboarding a client on docs.majesticone.co.uk, and it is the same whoever does it.

That covers deciding whether a site is a rebuild or a fresh build, surveying what the old site does before it goes away, creating the client in the platform, the Cloudflare Pages project, DNS, Search Console, and the launch-day sweep.

This document is only about you: getting set up, how work moves from your machine to a preview URL, and the rules that apply whatever you're building.


#Definition of done for a page



#Rules that don't bend

Never invent a trust claim. Not reviews, review counts, ratings, testimonials, accreditations, certifications, insurance, memberships, years in business, project numbers, awards or case studies. Every one comes from the client. This is strictest on fra-dorset-site, which is fire safety, and it applies to anything AI-generated.

If you need a number and don't have it, ask. Don't estimate, and don't write a realistic-looking placeholder — placeholders ship.

Preserve URLs when rebuilding. Covered in Phase 5. Check Search Console before you decide the structure. Rebuild at the old URLs; don't redirect to a tidier scheme.

Check what a page ranks for before changing its title. A homepage title rewrite lost HAL the "construction company" position it held. The platform warns you now, but the habit matters more than the guardrail.

Secrets never enter the repo. .env is gitignored, .env.example holds placeholders. Real values live in Cloudflare and Vercel only. Never paste one into a chat, a commit, a ticket — or a command line, where the shell saves it to history and any process can read it from ps. PUBLIC_* vars are the exception: they're inlined into the browser bundle by design and are meant to be visible.

Read the config before you quote it. Node version, build command, env var name, package version — open package.json / astro.config.mjs and look. A guessed config value is a broken deploy.

Structural things stay in the template. Icons, slugs, image paths, schema, template type, robots and sitemap URLs must never come from editable page data, because an AI content rewrite can wipe them. This has caused production bugs more than once.


#What's already protecting you

scripts/verify-build.mjs — wired into npm run build on every client site. Blocks broken internal links, blank pages and cross-domain contamination.

The daily self-test — checks every Google connection, every client's data freshness, and every paid provider each morning by actually calling them, and emails when something breaks. It exists because six of seven Google connections once ran dead for seven weeks while all of them reported "active", and nothing ever asked.

migration-kit/setup-client.mjs — configures a Cloudflare Pages project in one command. Run it with --dry-run first.

migration-kit/verify-analytics.mjs — loads a deployed page in a real browser and asserts the GA4 hit fires.


#You don't need a Cloudflare account

This surprises people, so it's worth stating plainly: you will never log in to Cloudflare, and you don't need an account of your own. One wouldn't help anyway — the projects live in the agency's account, not yours.

Deploys still work, because the Pages project is connected to the GitHub repo:

push a branch  →  Cloudflare builds it automatically  →  preview URL appears

That's the whole loop. Nothing to click, nothing to upload, no login.

The Cloudflare-side jobs are all one-time or launch-day, and they're Oliver's: creating the Pages project, setting environment variables, adding the custom domain, and setting CANONICAL_HOST when the site goes live.

If a build fails or a preview doesn't appear, that's a thing to report rather than a thing to go and fix — you can see the failure in the pull request without needing the dashboard.


#What you won't have access to, and why

None of this is about trust. These are the actions where a mistake can't be undone from the interface.


#When something looks wrong

Check it in a browser before you believe it. Several of the worst bugs this project has shipped were invisible in the code and obvious on screen: white text on a white background, an image at the wrong aspect ratio, a booking form with no handler that silently did nothing.

If a number looks too good, check it against Search Console. The rank tracker once reported a page at #1 that was nowhere in a real search.

Say when something failed. "I tried X, it did Y, I don't know why" is a completely fine report, and much cheaper than the alternative.

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.