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:
- Push to GitHub — it has no credentials. You push.
- Change DNS — no registrar login, and this is the step that can take a client's email down.
- Enter passwords or create accounts.
- Touch the production database — schema changes ship as SQL that Oliver pastes into Supabase.
#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.
- Go to github.com/signup.
- Use an email you'll keep. A personal address is fine.
- Pick a username — it appears on every commit you make, so keep it professional.
- Verify the email GitHub sends you. An unverified account can't be added to a repo.
- 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.
- 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.
- Repository invite — sent to your GitHub email. Click Accept. An unaccepted invite looks exactly like no access, and this is the single most common way day one stalls.
- Platform login for portal.majesticone.co.uk. He creates it on the Team page and gives you the password in person or via a password manager — never over email or chat. Change it once you're in. You'll be a member, not an admin: you can see every client and every number, and do all the day-to-day work, but not change who has access or connect Google accounts.
- Search Console access for your first client, if they already have a site
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:
- Create the client repo —
client-templateon GitHub → Use this template → name it<client>-site, set Private. It arrives carryingCLAUDE.md,BUILD-BRIEF.mdand the README already. - Invite them — that repo → Settings → Collaborators → Add people → their username.
- 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.
- Decide how
mainis protected before the site goes live. A collaborator has push access, and a push tomainis 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. - Fill in
BUILD-BRIEF.mdand commit it, or send it over. - 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:
- 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.
- 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:
- Nothing on a branch is live. Only merging to
maindeploys. - Cloudflare builds the branch to its own preview URL, so the review is the rendered site, not files someone has to build themselves to look at.
- The verify gate has already run before anyone spends time looking.
- A zip loses the git history, and
.enveither goes missing or — worse — goes with it.
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
-
npm run buildpasses - Loaded in a browser and looked at, mobile width included
- Every internal link resolves
- No invented trust claims anywhere on it
- Existing ranking URLs preserved
- Contact form submits and the lead actually arrives in the dashboard
- Nothing structural moved into editable page data
#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
- Cloudflare DNS and nameservers. Moving a domain's nameservers can kill the client's email, not just their website. A website rolls back in two minutes; lost email doesn't. This is the specific reason the Cloudflare login stays with Oliver rather than being shared out — not the deploys, which need no login at all.
- Production secrets — database URLs, API keys, OAuth tokens.
- Deleting leads, connecting ad accounts, managing users. A lead is the proof the whole service works.
- Approving a change the platform flags as risking a live ranking. You can always reject it, and rewording it to keep the ranking phrase is usually the better answer anyway.
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.