orphanage v0.1 · MIT

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+

report.html — 12 discovered · 11 fetched real run

sitemap only both agree crawl only

sitemap: 10 URLscrawl: 9 pages

2 found
Orphans

/gone · /orphan — declared, unreachable from the root

1 found
Unlisted

/secret — live and linked, absent from the sitemap

1 found
Broken internal link

/missing 404 — linked from /about

3 found
Redirect chain · deep page · dead sitemap entry

/old-contact → /contact · /blog/post-4 at depth 4 · /gone returns 410

excluded
1 page skipped by robots.txt

/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.

6finding kindseach with its own section
40 → 0false positives cuton the real run below
1real finding surfacedonce the noise was gone
0JS in the reportprints to PDF intact

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.

fixture site · 12 URLs discovered solid = link followed red = broken or orphaned blue = live, not in sitemap amber = redirected
Crawl graph of the fixture site Nine pages reachable by following links from the root. Two pages, /orphan and /gone, are declared in the sitemap but have no inbound link, so they float unconnected. One page, /secret, is reachable but missing from the sitemap. One link points at /missing, which returns 404. / ROOT about blog docs team post-3 guide 404 post-4 DEPTH 4 contact REDIRECTED secret NO INBOUND LINK orphan gone IN SITEMAP.XML UNREACHABLE

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.

first run 40

"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.

the fix 2

New buckets, not new thresholds: canonical-consolidated URLs and non-HTML resources are separated out and labelled rather than counted as findings.

after 1

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
If the crawl hits its page cap, the report states it beside the orphan and unlisted sections — a truncated crawl cannot tell a genuine orphan from a page it simply had not reached yet.
02 A robots-excluded page is shown as excluded, not broken
The tool found a link to it and chose not to follow it. That is a different fact from "this link is broken", and it gets a different row.
03 A dead sitemap entry needs an observed status, not an assumed one
A sitemap URL that was neither crawled nor separately checked is reported as unknown status. It is never guessed at, and never counted as dead.
04 The report ships open, with no JavaScript
Every collapsible section is open by default, because a collapsed <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.

bash
$ 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.

N01

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.

N02

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.

N03

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.