fix(seo,geo): C1c — split COVERAGE source/live; the 30/70 rule needed the

opposite sample

I5 made COVERAGE mandatory and told the agent to "sample by risk, one per
template". Grounding that against a real Astro site shows the rule is half
wrong, and that one number was hiding two.

86 URLs collapse into 8 families; 75 of them (87%) come from 3 dynamic
[dept] templates. So the same 12 sampled pages are simultaneously 14% LIVE
coverage and ~100% SOURCE coverage. Reporting only 14% understates the audit;
reporting only 100% oversells it. Both lines now, in both agents — and they
bound different findings, so they must not be averaged:
  SOURCE bounds CODE (one template renders its whole family: a missing
  canonical in [dept]/index.astro breaks all 25 identically).
  LIVE bounds CONTENT (title wording, thin pages, duplication — written per
  page, so a template says nothing about its 25 instances).

The sharper half: "one per template" is CORRECT for code and WRONG for the
30/70 rule, which this same spec mandates at :954 (city pages: 30% shared,
70% unique). You cannot tell whether 25 dept pages are 70% unique by reading
one of them. The spec was mandating a check its own sampling made
structurally impossible. Sampling is now keyed to the finding class: 1 per
family for code, >=3 from the LARGEST family for duplication, spread for
per-page content.

Families come from the first path segment of the sitemap URLs (C1b) — a good
enough proxy for "same template" that needs no framework routing knowledge,
verified against the real distribution.

geo gets the same split, cut differently: JSON-LD lives in a shared layout so
SOURCE bounds Schema.org, while Definition Lead / TL;DR are written per page
so LIVE bounds Content Shape. Site-wide axes (crawler policy, llms.txt) stay
unbounded — single files, fully read.

Verified: full suite green. Caught and fixed one self-inflicted contradiction
before commit — geo's prose demanded both coverages while its output block
still had a single line.
This commit is contained in:
Bastien Chanot
2026-07-17 11:43:48 +02:00
parent 2de58faa38
commit 04ccc5ad9b
2 changed files with 56 additions and 13 deletions
+43 -12
View File
@@ -479,11 +479,25 @@ point of use (same contract as the sameAs check in geo-analyzer).
### Meta tags per page (sample 5-15 key pages)
Sample by risk, not convenience: homepage + top templates (one per page
type: service, city, blog, product, legal) + any page GSC flags as a
position 4-10 quick win. Same template audited twice buys nothing; an
un-sampled template is an un-audited template — name the templates you
skipped.
**Group the sitemap URLs into families first** — first path segment is a
good enough proxy for "same template", and it needs no framework routing
knowledge. Measured on a real Astro site: 86 URLs collapse into 8 families,
and 75 of them (87%) come from just 3 dynamic `[dept]` templates.
**Sample by finding class, because the classes need opposite samples:**
| Looking for | Sample | Why |
|---|---|---|
| Code defects (canonical, OG, `<img>` dims, hreflang) | **1 per family** | one template renders the whole family — a missing canonical in `[dept]/index.astro` breaks all 25 identically. 1 per family ≈ 100% SOURCE coverage for ~8 fetches. |
| **Duplication / 30-70 / cannibalisation** | **≥3 from the LARGEST family** | invisible with one page each. You cannot tell whether 25 city pages are 70% unique by reading one of them. |
| Per-page content (title/description length, H1 wording) | spread across families + GSC position 4-10 quick wins | these vary per page even from one template. |
"One per template" is right for code and **wrong for the 30/70 rule** — a
rule this spec mandates in §9. Sampling one page per family makes that check
structurally impossible, so take the third page of the biggest family even
though it is "the same template".
An un-sampled family is an un-audited family. Name the ones you skipped.
For each sampled page:
```
@@ -868,8 +882,9 @@ misroutes the client-handover gate and the user's effort.
```
SEO SCORING (<depth>)
COVERAGE : <N> of <M> sitemap URLs (<P>%) — templates skipped: <list|none>
| <N> pages, total UNKNOWN (no sitemap)
COVERAGE SOURCE: <N> of <M> page templates (<P>%) — skipped: <list|none>
COVERAGE LIVE : <N> of <M> sitemap URLs (<P>%) — families: <fam N/M, …>
| UNKNOWN (no sitemap / fetch degraded)
Technical : XX/20 <justification>
On-page : XX/20 <justification>
SEO Local : XX/20 | N/A
@@ -881,11 +896,27 @@ Legal : XX/20 <justification>
SEO GLOBAL (weighted): XX.X/20 (<depth>)
```
**COVERAGE is mandatory, never omitted, never rounded up.** It is the
honesty bound on every page-level axis: On-page and the on-page share of
Technical are extrapolations from the sample. If coverage < 25%, repeat it
in §0 as a major alert — a 17/20 drawn from 3% of a site is not a 17/20, and
`/client-handover` gates on these numbers.
**Both COVERAGE lines are mandatory, never omitted, never rounded up.** They
are the honesty bound on every page-level axis: On-page and the on-page share
of Technical are extrapolations from the sample, and `/client-handover` gates
on these numbers.
**Report both, because they bound different findings — do not average them
into one comforting number.**
- **SOURCE** bounds CODE findings. One template renders its whole family, so
1 page per family can legitimately reach 100% here. High SOURCE coverage is
a real claim: the code paths were seen.
- **LIVE** bounds CONTENT findings — title/description wording, thin pages,
30/70 duplication. It stays low by design and that is fine, as long as it
is printed. Measured on a real site: 12 of 86 URLs is 14% LIVE while the
same 12 pages are 100% SOURCE. Reporting only the 14% understates the audit;
reporting only the 100% oversells it. Both, or neither means anything.
- LIVE < 25% → repeat in §0. A 17/20 for content drawn from 3% of a site is
not a 17/20.
- SOURCE < 100% → name the skipped templates in §0. That is not a sampling
choice, it is code nobody read.
- Denominator UNKNOWN (no sitemap, or `sitemap` degraded) → print UNKNOWN.
Never let silence imply full coverage.
Per user instruction: this score represents **80% of the combined
final score for local B2C (20% for GEO), or 75% for SaaS/national