The Pre-Deployment SEO Checklist for Developers
By Seth Black · Published
Most SEO problems I get pulled into started life as a routine PR. Someone refactored the router, swapped a CMS field, or moved canonical logic into a shared component. It looked fine in staging. It shipped, and three weeks later someone in marketing noticed traffic was down and asked what changed.
The fix isn’t more meetings. It’s a checklist that runs before merge, the same way tests and linters do. Here’s the one I use, and how to move it into CI so nobody has to remember it.
Before you open the PR
Run these against your local build. None of them take more than a minute.
1. Canonical sanity check
Look at what’s actually in the HTML, not what the CMS preview says.
curl -s http://localhost:3000/some-page | grep -i canonical
If the canonical points at localhost, a staging hostname, or a URL that doesn’t match the page, stop and fix it. This is the most common regression I see, and it ships because nobody looked. Check a deep link loaded directly, not just a page you clicked to from the homepage, because client-side head managers often get this wrong on cold loads.
2. Status codes for the routes you touched
If you changed routing, redirects, or middleware, hit the old URLs and the new ones.
curl -sI http://localhost:3000/old-url | head -n 1
A 301 should be a 301. A removed page should be a 404 or 410. A 200 on a page that no longer exists is how you end up with thousands of soft 404s in Search Console six months later. If a redirect goes through more than one hop, point it straight at the final URL.
3. Title and meta description in the raw HTML
View source and confirm <title> and <meta name="description"> exist in the HTML the server sends, not just in the rendered DOM. If they only appear after JavaScript runs, write it down and deal with it before merge.
4. Robots and noindex
Search the diff for noindex, robots, and X-Robots-Tag. Staging environments love to leak noindex into production. I watched a team ship a config change that flipped an entire site to noindex because one environment variable was missing in production. It took four days to notice. Check robots.txt too, including rules that block the JavaScript and CSS your pages need to render.
5. Internal links resolve
If you added, moved, or removed pages, make sure nothing still links to the old URLs. A quick crawl of your local build catches most of these, and it’s much cheaper now than after the links are live.
Before you merge
Two more things, both fast.
- Diff raw versus rendered HTML on any page your change touched. If your H1, canonical, or structured data only exist after JavaScript runs, Google has to finish rendering to see them. Document it at minimum. The JavaScript rendering issues post covers what belongs in the initial HTML.
- Check the sitemap output. If your build generates a sitemap, regenerate it and look at the diff. New pages should appear, removed pages should disappear. Surprises mean your sitemap logic is out of sync with your routes.
Move it into CI
A checklist in a Notion doc gets skipped exactly when it matters: the big release, the rushed hotfix. Automate the parts a crawler can check, and fail the build when they regress.
The pattern: crawl staging, write JSON, compare against a baseline you trust, and exit non-zero if something important got worse.
black-seo-analyzer \
--url-to-begin-crawl "$STAGING_URL" \
--max-pages 1000 \
--output-type json \
--output-file audit.json
python scripts/check_seo_regressions.py audit.json baseline.json || exit 1
Your regression script decides what counts as a failure. Mine usually fails on:
- Any URL that was indexable in the baseline and now has
noindex - Any canonical pointing at a non-200 URL or a hostname you don’t own
- More internal 404s than the baseline
- The indexable page count dropping by more than 2%
That last rule catches the systemic stuff. If a template change orphans a thousand product pages, the count drop shows up before anyone files a ticket.
The baseline file is the trick
baseline.json lives in the repo. You update it deliberately, the same way you update a snapshot test, and the change gets reviewed in a PR. That’s the whole governance model.
The trap is regenerating the baseline on every run. Then your regression check compares the site to itself and never catches anything. Pin it, and update it on purpose.
Where to put it
Run it after staging is built and before the rollout. In GitHub Actions that’s a job step. In GitLab or Jenkins, it’s a stage. The crawl takes a few minutes on a small site and longer on a big one, so if pipeline time is a concern, run it on the deploy-to-staging job instead of every commit.
If your team won’t accept a hard fail on SEO yet, run it as a warning for two weeks and see what it catches. People stop arguing once it catches a real one.
For the rest of the CLI setup, including JSONL summaries, severity filters, and Slack alerts, see how to run a technical SEO audit from the command line.
What done looks like
A programmer pushes a change that adds noindex to the layout template. CI crawls staging, the script finds fifty newly noindex URLs that were indexable in the baseline, and the build fails with a message naming them. The fix takes ten minutes. Production never sees it, and neither does Search Console.
SEO regressions are code regressions. Treat them that way and the panicked threads about traffic drops mostly stop.
BSA ships a real CLI with JSON output for exactly this. Read the CLI docs, or try it free for 14 days with every feature and no page limit.
-Sethers