Abdelrahman

next-seo-doctor

SEO breaks silently. No error, no console warning — just quietly worse results months later. next-seo-doctor crawls your built Next.js site the way a search engine would, and tells you what it actually found, not what your code intended.

bash
npx next-seo-doctor http://localhost:3000

GitHub ↗ · npm ↗ · How I wired this site's own SEO

What it does

Most SEO tooling checks what your code is supposed to render. This one checks what actually shipped: it starts your site, crawls the sitemap, and reads the served HTML — canonical tags, JSON-LD, robots.txt, Open Graph meta — the same first pass a crawler gets before any client-side JavaScript runs. If something's missing, malformed, or lying about itself, it shows up here instead of three months from now in a Search Console graph that's quietly trending down.

Here's real output from a site with a few problems:

terminal
canonical 42/42 pages ok
sitemap-honesty 38/42 lastmod values identical (2026-07-01T00:00:00.000Z)
jsonld-valid 61 blocks parsed
jsonld-graph 2 dangling @id references
jsonld-xss 61/61 blocks escaped
robots-leak Disallow: /admin/
og-image 41/42 ok
duplicate-canonicals no collisions

Quickstart and usage

The tool crawls a running build, not your source files, so build and start it first:

bash
next build && next start
npx next-seo-doctor http://localhost:3000
FlagDefaultPurpose
--max-pages <n>200Crawl limit
--concurrency <n>5Parallel fetches
--filter <prefix>Restrict the crawl to sitemap URLs whose pathname starts with this prefix
--only <ids>Comma-separated check IDs; run only these
--skip <ids>Comma-separated check IDs; run all except these
--jsonoffMachine-readable output instead of the colored report
--versionPrint version and exit
--helpPrint help and exit

Exit codes are the whole point if you're wiring this into CI:

Since the exit code already reflects pass/fail, a CI step needs nothing special — just run it and let a non-zero exit fail the job:

bash
npx next-seo-doctor https://staging.example.com

The 8 checks

canonical

Catches: pages with no <link rel="canonical">, a relative canonical href, a canonical pointing at a different origin than the one crawled, or a canonical that doesn't self-reference the page it's on.

Why it matters: canonicals are how you tell search engines "this is the one true URL for this content." Get it wrong and the engine picks for you — often a staging domain, a query-string variant, or nothing at all — splitting ranking signals across duplicates instead of consolidating them.

duplicate-canonicals

Catches: two or more different pages whose canonical tags point at the same URL.

Why it matters: duplicate canonicals mean those pages are competing against each other in the index instead of pointing at one winner. Common with pagination, filtered category views, or a template bug that hardcodes a canonical instead of deriving it per page.

jsonld-valid

Catches: JSON-LD <script> blocks that fail to parse as JSON.

Why it matters: a single unclosed brace and the entire structured-data block silently stops being read. Not a build error, not a console warning — Google just skips it, and you find out when a rich result you expected never shows up.

jsonld-graph

Catches: @id references that point at a node nothing on the page declares (dangling references), and pages with multiple typed schema nodes that never link to each other at all.

Why it matters: isolated schema nodes — a Person here, a Product there, nothing connecting them — read as confetti, not an entity graph. The whole value of @id linking is telling Google these facts describe the same thing; skip it and you're back to disconnected guesses.

jsonld-xss

Catches: unescaped < characters or a literal </script sequence inside a JSON-LD block.

Why it matters: this is a real script-breakout vector, not theoretical. Any title, description, or FAQ answer pulled from user input or a CMS can close your <script> tag early and inject markup if you serialize with plain JSON.stringify and skip the escape step.

robots-leak

Catches: Disallow entries in robots.txt containing keywords like admin, secret, .env, staging, internal, backup, config, dashboard.

Why it matters: robots.txt is public. Listing a path there to "hide" it does the opposite — it hands crawlers, and anyone who's curious, a map of exactly what you didn't want found. Disallow is a crawling instruction, not authentication.

og-image

Catches: pages missing an og:image tag, images that return a non-2xx response, and images that aren't exactly 1200×630.

Why it matters: link previews break per page, not site-wide — most of your site can have perfect Open Graph images while one template quietly ships a 404 or the wrong aspect ratio, and you won't notice until someone pastes the link in Slack and gets nothing.

sitemap-honesty

Catches: sitemaps where under half the entries have a lastmod value at all, or where 80%+ of entries share one identical timestamp.

Why it matters: lastModified: new Date() at build time makes every page look like it changed today, on every deploy. Crawlers notice the pattern and start ignoring your lastmod entirely — including the times it's actually telling the truth about a real update.

Fixing what it finds

The jsonld-graph and jsonld-xss checks above tell you that your structured data is broken. For copy-ready, @id-linked JSON-LD templates covering 7 site categories — blog, e-commerce, SaaS, local business, real estate, portfolio, and docs — see the JSON-LD schema guide.

Beyond the checks

A few things worth knowing that the tool won't catch directly. Canonicals are an identity claim, not a decoration you paste onto every page — get the mental model right and most of next-seo-doctor's canonical findings stop happening in the first place. The same root cause — not setting metadataBase — is behind most broken-canonical bugs I've seen, including a few of my own. Sitemap lastModified only has value if it's actually true; OG images need to be exactly 1200×630, not "close enough." And remember robots.txt is publicDisallow is not a substitute for authentication, ever.

Last thing: verify, don't assume. Run your production URL through Google's Rich Results Test, search site:yourdomain.com for what's actually indexed, and check Search Console — none of these tools, including this one, replace looking at what search engines actually did with your site.

FAQ

Does it work on non-Next.js sites?

Mostly yes. The checks themselves validate the served HTML — canonical tags, JSON-LD, robots.txt, OG images — which are HTTP/HTML concepts, not Next.js-specific. The fix hints in the output (mentioning metadataBase, sitemap.ts) speak Next.js, so they'll need translating for another framework.

Why does everything fail on my CSR (client-rendered) site?

That's the finding, not a bug. The tool reads the server-rendered HTML the same way a crawler's first pass does — if your canonical tag, JSON-LD, and OG meta only appear after client-side JavaScript runs, they weren't there when it checked, and likely weren't there for a lot of real crawlers either.

Can I run it in CI?

Yes — that's the exit codes' whole purpose. 0 is clean, 1 means SEO errors were found, 2 means the tool itself couldn't run. Wire it as a normal CI step against a staging deploy and let the exit code fail the job.

Does it execute JavaScript?

No. It reads the server HTML response directly, the same way a crawler's first pass does. Anything that only appears after hydration won't be seen — which is also why the CSR question above isn't really a bug report.

Why did og-image flag my image when it looks fine in the browser?

The check fetches the actual image bytes and reads their real dimensions — it doesn't trust what the browser renders. A redirect, a wrong content-type, or a source image that isn't exactly 1200×630 will get flagged even if it looks acceptable in a manual preview.

Is it free?

Yes, MIT licensed.


If this saved you from shipping a silent SEO regression, a star on GitHub helps other people find it. And if you want the full story of how this site wires its own SEO — the same checks this tool runs — read the write-up.