feat(seo-data): C3 — internal link graph; orphans + click depth, measured
seo-analyzer.md:613 asks "Every important page reachable within 3 clicks?"
and :616 asks "Orphan pages (no inbound internal links)?". Neither ever had a
command — same shape as the sameAs check before W3. This is that command.
My earlier reservation ("costs a lot of network") was wrong and the
measurement killed it: 24 pages in 2.7s, 86 in 3.8s. Cheap enough to always
run on FULL.
EXHAUSTIVE OR NOTHING is the design constraint, not a nicety. Orphans cannot
be sampled: proving a page has no inbound link means having read every other
page. So when the crawl is capped or any page fails, orphans are WITHHELD —
`orphans_withheld: true` and no list. A false orphan ("page X has no inbound
links" when it does) sends a client fixing what is not broken; that is the
worst finding this tool could emit. The cap does not degrade the result, it
invalidates it.
SPA refusal: on a client-rendered site the links are not in the HTML and
every page reads as orphaned. That is catastrophic, so an empty graph returns
degraded/no_links_in_html instead of a full false-positive list. No JS
rendering by design — that is the R1/R2 arbitration, not something to smuggle
in here.
Verified against BOTH live sites and against a planted failure, because two
clean results are not evidence a detector detects:
- native PHP: 24 pages, 335 links, depth 2, 0 orphans
- Astro: 86 pages, 2015 links, depth 2, 0 orphans
- fixture with a planted orphan + a 4-click chain: both found. Filters proven
on real shapes seen live — /css/main.css?v=1778157313, #anchors, mailto:,
tel:, external hosts, .png. /b/ in markup vs /b in sitemap unify to one node
rather than a phantom orphan pair.
Fixed a flaw in my own mock while writing that test: a single page.html
fixture cannot express a GRAPH (every node gets identical links), so the mock
is now pages.json = {url: html}.
Verified: seo-data 122 -> 136 pass, 0 fail; full suite green; shellcheck +
py_compile clean.
This commit is contained in:
@@ -624,6 +624,34 @@ Each embedded or self-hosted video should have:
|
||||
|
||||
### Internal linking + topic clusters (silos sémantiques)
|
||||
|
||||
```bash
|
||||
bash ~/.claude/lib/seo-data/fetch.sh linkgraph --url "https://$DOMAIN/sitemap.xml"
|
||||
```
|
||||
|
||||
**This answers the two questions below, which this spec has always asked and
|
||||
never had a command for (C3).** Crawls every sitemap URL once, extracts
|
||||
internal `<a href>`, and returns `orphans`, `beyond_3_clicks`, `unreachable`,
|
||||
`max_depth`. Measured cost: 24 pages in 2.7 s, 86 in 3.8 s — cheap enough to
|
||||
always run on FULL.
|
||||
|
||||
Read it honestly:
|
||||
- `orphans` present → real finding, act on it.
|
||||
- **`orphans_withheld: true` → there is NO orphan list, and you must not
|
||||
invent one.** It appears when the crawl was capped or any page failed. An
|
||||
orphan cannot be sampled: proving a page has no inbound link means having
|
||||
read every other page, so a partial crawl invents orphans. "Page X has no
|
||||
inbound links" when it does sends the client fixing what is not broken.
|
||||
§14 line, not a finding.
|
||||
- `reason: no_links_in_html` → **not a site with zero links; a site whose
|
||||
links are rendered by JS.** Every page would look orphaned — the worst false
|
||||
positive this tool could emit — so the verb refuses instead. Flag the SPA in
|
||||
§0 and stop; do not hand-roll a link audit around it.
|
||||
- `unreachable` ⊃ `orphans`: a page can have inbound links yet sit outside the
|
||||
homepage's reach (linked only from another unreachable page). Both matter,
|
||||
they are not the same finding.
|
||||
- `max_depth` > 3 → `beyond_3_clicks` names the pages. That is the ":613"
|
||||
check, now measured rather than asserted.
|
||||
|
||||
Sample critical pages. Check:
|
||||
- Every important page reachable within 3 clicks from homepage?
|
||||
- Navigation consistent?
|
||||
|
||||
Reference in New Issue
Block a user