"unlisted" pages reported. 35 were /console?c=… permalinks all declaring rel=canonical back to one page; 5 were non-HTML endpoints like llms.txt and a CV PDF.
crawl graph vs. sitemap
Some of your pages are declared but unreachable.
orphanage crawls from the root the way a crawler does, reads your sitemap separately, and reports only where the two disagree. Six finding categories, no dashboard, no account.
✓ zero runtime deps ✓ respects robots.txt ✓ Node 20+
/gone · /orphan — declared, unreachable from the root
/secret — live and linked, absent from the sitemap
/missing 404 — linked from /about
/old-contact → /contact · /blog/post-4 at depth 4 · /gone returns 410
/private/page — shown as excluded, never counted as broken
A committed fixture site, built to populate all six categories at once — so you can see every section without trusting a screenshot.
what an orphan actually is
A page can be in the sitemap and still have no way in.
The graph is the crawl. Solid edges are links that were actually followed from the root. The two dashed nodes are declared in sitemap.xml and have no inbound edge at all.
the run that nearly shipped
Forty findings. Every single one was noise.
Run against agentjames.vercel.app on 2026-09-05. A tool that buries one real finding under forty false ones is worse than no tool — which is why the canonical and content-type buckets exist at all.
New buckets, not new thresholds: canonical-consolidated URLs and non-HTML resources are separated out and labelled rather than counted as findings.
Unlisted dropped to zero, and the finding that mattered became visible: /games is in the sitemap and is not reachable by following any link from the homepage.
enforced mechanically
Three rules that stop it from guessing.
orphanage --check <report.html> re-runs the structural rules against any report, including one you did not generate.
01 A truncated crawl says so, next to the findings it invalidates
02 A robots-excluded page is shown as excluded, not broken
03 A dead sitemap entry needs an observed status, not an assumed one
unknown status. It is never guessed at, and never counted as dead.04 The report ships open, with no JavaScript
<details> disappears from a printed PDF. overflow-wrap: anywhere is set on every URL cell so one long URL cannot push the layout wider than a phone.two commands
Crawl it, then let CI verify the report.
$ node bin/orphanage.mjs https://example.com/
crawled 9 pages · sitemap declared 10 URLs
orphans 2 /gone /orphan
unlisted 1 /secret
broken links 1 /missing (404, from /about)
wrote report.html — exit 1
$ node bin/orphanage.mjs --check report.html
✓ structural rules hold
exit codes
0 nothing to report 1 a finding was produced, or the report failed its own--check
2 used wrongly, so nothing was measured
stated up front
What it will not tell you.
Whether GPTBot or Googlebot can reach a page
This tool crawls once, with one ordinary browser-style User-Agent. Per-crawler access is a separate method, handled by a sibling tool built for exactly that.
Anything behind client-side navigation
It sees exactly what a crawler that does not execute JavaScript sees. Links injected at runtime are invisible to it, and it does not pretend otherwise.
Whether a page deserves to be indexed
A correctly-orphaned thank-you page still appears in the orphans list. Deciding that is a judgment call for a person, not for this tool.