feat(client-handover): split SEO + GEO scores, gate GEO at ≥17/20

This commit is contained in:
bastien 2026-05-07 18:34:59 +02:00
parent 64cef26e50
commit c99308f0a7
2 changed files with 184 additions and 54 deletions

View File

@ -190,7 +190,9 @@ fresh in `.claude/audits/`).
If a fresh `.claude/audits/SEO.md` or `.claude/audits/HARDEN.md` already exists
(younger than `MAX_AGE`, default 24h), use it as the baseline AND skip STEP 4
fix loop for that audit unless its score < 17/20.
fix loop for that audit unless its score < 17/20. For SEO.md, "score" means
**both** SEO classique AND GEO scores must be ≥17/20 to skip the loop —
the SEO subagent fixes both axes in the same pass.
```bash
mkdir -p .claude/audits
@ -214,7 +216,7 @@ For web projects, dispatch in **a single message with two parallel Agent calls**
| Audit (web) | Subagent | Prompt template |
|---------------|-------------------|-----------------|
| SEO + GEO | `general-purpose` | "Read `~/.claude/skills/seo/SKILL.md` and execute it on this project. The /seo skill runs SEO + GEO in parallel and writes a unified report to `.claude/audits/SEO.md`. Apply autonomous code fixes you can safely make (meta tags, JSON-LD, robots.txt, sitemap.xml, llms.txt, alt attrs, canonical tags). At the top of the report include exactly one line: `Score: X/20` (or `X/100` — the agent will normalize). Return when the report file is written." |
| SEO + GEO | `general-purpose` | "Read `~/.claude/skills/seo/SKILL.md` and execute it on this project. The /seo skill runs SEO + GEO in parallel and writes a unified report to `.claude/audits/SEO.md`. Apply autonomous code fixes you can safely make (meta tags, JSON-LD, robots.txt, sitemap.xml, llms.txt, alt attrs, canonical tags). At the top of the report, the /seo skill MUST emit two distinct labeled score lines (already specified in its SKILL.md §1): `Score SEO (classique) : X.X / 20` and `Score GEO (IA) : X.X / 20`, plus the weighted global. The handover orchestrator parses SEO and GEO separately, so do not collapse them into a single `Score:` line. Return when the report file is written." |
| HARDEN | `general-purpose` | "Read `~/.claude/skills/harden/SKILL.md` and execute it on this project. Apply autonomous code fixes (security headers in vercel.json/netlify.toml/.htaccess/nginx.conf, HSTS, CSP defaults, HTTP→HTTPS redirects, canonical, 404 page). Write report to `.claude/audits/HARDEN.md` with `Score: X/20` (or `X/100`) at the top. Return when the report file is written." |
Non-web variant:
@ -227,25 +229,70 @@ Wait for both subagents to complete (parallel return).
### Parse baseline scores
The `/seo` skill writes a unified `.claude/audits/SEO.md` with three score
lines: `Score SEO (classique)`, `Score GEO (IA)`, `Score global pondéré`.
The handover doc reports SEO and GEO separately (see STEP 8 + STEP 12 §4)
and **gates them independently** — both must reach ≥17/20 for the
pipeline to pass. Extract each as a distinct variable.
```bash
# Generic extractor: matches any "Score: X/20" or "X/20" or "X/100" line.
# Use for HARDEN, VALIDATE, CSO (single-score reports).
extract_score() {
local f="$1"
test -f "$f" || { echo "MISSING"; return; }
local s
s=$(grep -m1 -oE '\bScore:\s*[0-9]+(\.[0-9]+)?\s*/\s*(20|100)\b' "$f" | head -1)
[ -z "$s" ] && s=$(grep -m1 -oE '\b[0-9]+(\.[0-9]+)?/20\b' "$f" | head -1)
[ -z "$s" ] && s=$(grep -m1 -oE '\b[0-9]+(\.[0-9]+)?/100\b' "$f" | head -1)
[ -z "$s" ] && s=$(grep -m1 -oE '\b[0-9]+(\.[0-9]+)?\s*/\s*20\b' "$f" | head -1)
[ -z "$s" ] && s=$(grep -m1 -oE '\b[0-9]+(\.[0-9]+)?\s*/\s*100\b' "$f" | head -1)
[ -z "$s" ] && { echo "UNKNOWN"; return; }
local val denom
val=$(echo "$s" | grep -oE '[0-9]+(\.[0-9]+)?' | head -1)
denom=$(echo "$s" | grep -oE '/[0-9]+' | tr -d '/')
denom=$(echo "$s" | grep -oE '/\s*[0-9]+' | tr -d '/ ')
if [ "$denom" = "100" ]; then
val=$(awk "BEGIN { printf \"%.2f\", $val/5 }")
fi
echo "$val"
}
SCORE_SEO_BEFORE=$(extract_score .claude/audits/SEO.md)
# Labeled extractor: pulls the score from a specific labeled line
# (e.g. "Score SEO" or "Score GEO" inside SEO.md).
# Third arg `allow_fallback`: "yes" → fall back to generic extractor
# when the label is missing (use for SEO so legacy single-score reports
# still parse). "no" → return UNKNOWN if label missing.
# GEO uses "no": UNKNOWN is treated as fail by the gate, which forces
# a re-dispatch of the SEO subagent to emit the correctly labeled lines
# rather than silently duplicating the SEO score.
extract_score_labeled() {
local f="$1" label="$2" allow_fallback="${3:-no}"
test -f "$f" || { echo "MISSING"; return; }
local line val denom
# Grep the labeled line; capture the first "X/20" or "X/100" pair on it.
line=$(grep -m1 -iE "$label[^0-9/]*[0-9]+(\.[0-9]+)?\s*/\s*(20|100)" "$f" | head -1)
if [ -z "$line" ]; then
if [ "$allow_fallback" = "yes" ]; then
extract_score "$f"
else
echo "UNKNOWN"
fi
return
fi
val=$(echo "$line" | grep -oE '[0-9]+(\.[0-9]+)?\s*/\s*(20|100)' | head -1 \
| grep -oE '[0-9]+(\.[0-9]+)?' | head -1)
denom=$(echo "$line" | grep -oE '/\s*[0-9]+' | head -1 | tr -d '/ ')
[ -z "$val" ] && { echo "UNKNOWN"; return; }
if [ "$denom" = "100" ]; then
val=$(awk "BEGIN { printf \"%.2f\", $val/5 }")
fi
echo "$val"
}
# SEO falls back to generic if label missing (legacy SEO.md compat).
# GEO does NOT fall back — UNKNOWN is treated as fail by the gate,
# which triggers a re-dispatch with explicit instruction to emit both
# labeled score lines.
SCORE_SEO_BEFORE=$(extract_score_labeled .claude/audits/SEO.md "Score SEO" yes)
SCORE_GEO_BEFORE=$(extract_score_labeled .claude/audits/SEO.md "Score GEO" no)
SCORE_HARDEN_BEFORE=$(extract_score .claude/audits/HARDEN.md)
# (non-web)
# SCORE_CSO_BEFORE=$(extract_score .claude/audits/CSO.md)
@ -258,33 +305,56 @@ Store these for the final doc's before/after table.
## STEP 4 — FIX LOOPS (parallel, bounded)
Skip if `--skip-fix-loop` or `--skip-audits`. Skip per-audit if its
`*_BEFORE` is already `≥17/20`.
`*_BEFORE` is already `≥17/20`. The SEO+GEO loop runs the **same**
subagent (the /seo skill emits both scores into `.claude/audits/SEO.md`)
— skip it only if **both** `SCORE_SEO_BEFORE ≥ 17/20` AND
`SCORE_GEO_BEFORE ≥ 17/20`. If either is below threshold, the loop
runs.
### Loop structure (per audit, runs concurrently with the other audit's loop)
```
MAX_ITERATIONS = 5 (override via --max-iterations N)
iteration = 1
while score < 17/20 and iteration MAX_ITERATIONS:
# SEO+GEO loop continues while EITHER score is below threshold.
# HARDEN/CSO/VALIDATE loops use only their own score.
while (audit == "SEO" ? (SCORE_SEO < 17 OR SCORE_GEO < 17) : score < 17) \
and iteration ≤ MAX_ITERATIONS:
re-dispatch the audit subagent with iteration context (see prompt below)
re-parse score from the updated audit file
if score == previous score and no files changed → break (no progress)
re-parse score(s) from the updated audit file
if no scores improved AND no files changed → break (no progress)
iteration += 1
```
### Re-dispatch prompt template (SEO loop)
### Re-dispatch prompt template (SEO + GEO loop)
Send to `general-purpose` subagent:
> Read `~/.claude/skills/seo/SKILL.md` and re-run it on this project.
> Previous score: **`<SCORE_SEO_PREVIOUS>`/20** — below threshold of 17/20.
> Iteration `<N>` of `<MAX_ITERATIONS>`.
> Previous scores:
> - **SEO classique: `<SCORE_SEO_PREVIOUS>`/20** (threshold 17/20 — `<PASS|FAIL>`)
> - **GEO (IA): `<SCORE_GEO_PREVIOUS>`/20** (threshold 17/20 — `<PASS|FAIL>`)
>
> Iteration `<N>` of `<MAX_ITERATIONS>`. Both axes are gated independently;
> the orchestrator continues to loop while EITHER score is below 17/20.
>
> Read `.claude/audits/SEO.md` for the current issue list. Apply ALL safe
> autonomous fixes (do not skip "easy" ones). For each fix applied, append
> a line to `.claude/audits/SEO-FIX-LOG.md` (format: `iter<N>: <issue>
> <file:line><action>`). Update `.claude/audits/SEO.md` with new score.
> Do NOT ask the user; apply or skip with one-line justification in the
> fix log.
> autonomous fixes (do not skip "easy" ones). Prioritize fixes for the
> axis currently below threshold:
> - SEO classique fixes: meta tags, headings, canonical, sitemap.xml,
> alt attrs, internal linking, Core Web Vitals hints.
> - GEO (IA) fixes: llms.txt / llms-full.txt, robots.txt entries for AI
> crawlers (GPTBot, ClaudeBot, PerplexityBot, etc.), Schema.org for AI
> extraction (QAPage, Speakable, Person+Article, HowTo, Organization
> graph), entity SEO (sameAs, @id), TL;DR / definition-lead content
> shape, citable stats markup, freshness signals.
>
> For each fix applied, append a line to `.claude/audits/SEO-FIX-LOG.md`
> (format: `iter<N>: [SEO|GEO] <issue> → <file:line> — <action>`). Update
> `.claude/audits/SEO.md` with the new scores — both labeled lines MUST
> be present: `Score SEO (classique) : X.X / 20` and
> `Score GEO (IA) : X.X / 20`, plus the weighted global. Do NOT ask the
> user; apply or skip with one-line justification in the fix log.
### Re-dispatch prompt template (HARDEN loop)
@ -342,11 +412,21 @@ AskUserQuestion:
(will be marked as caveat in client doc)
```
For the SEO + GEO loop (single subagent, two gated scores), label the
prompt with **both** axis scores when one or both are below threshold,
e.g. `"SEO+GEO loop stuck — SEO classique 17.2/20 ✅, GEO (IA)
14.5/20 ❌ — after 5 iterations. ..."`. Option C overrides only the
axis the user names (SEO, GEO, or both) — record per-axis overrides
in `.claude/audits/THRESHOLD-OVERRIDE.md`.
Per user instructions (radical honesty, no temp fixes), **default
recommendation is B**. Only choose C with explicit user consent.
After loops finish (success, stall, or override), capture:
- Web: `SCORE_SEO_AFTER`, `SCORE_HARDEN_AFTER`
- Web: `SCORE_SEO_AFTER`, `SCORE_GEO_AFTER`, `SCORE_HARDEN_AFTER`
- `SCORE_SEO_AFTER=$(extract_score_labeled .claude/audits/SEO.md "Score SEO" yes)`
- `SCORE_GEO_AFTER=$(extract_score_labeled .claude/audits/SEO.md "Score GEO" no)`
- `SCORE_HARDEN_AFTER=$(extract_score .claude/audits/HARDEN.md)`
- Non-web: `SCORE_CSO_AFTER`
---
@ -487,11 +567,18 @@ Compute final score table.
**Web project:**
| Audit | Before | After | Status |
|----------|---------------------------|------------------------|----------------|
| SEO | `SCORE_SEO_BEFORE`/20 | `SCORE_SEO_AFTER`/20 | ✅ ≥17 / ❌ <17 |
| HARDEN | `SCORE_HARDEN_BEFORE`/20 | `SCORE_HARDEN_AFTER`/20| ✅ / ❌ |
| VALIDATE | — | `SCORE_VALIDATE_AFTER`/20 | ✅ / ❌ / SKIPPED |
| Audit | Before | After | Status |
|-------------------|---------------------------|-----------------------------|-------------------|
| SEO (classique) | `SCORE_SEO_BEFORE`/20 | `SCORE_SEO_AFTER`/20 | ✅ ≥17 / ❌ <17 |
| GEO (IA) | `SCORE_GEO_BEFORE`/20 | `SCORE_GEO_AFTER`/20 | ✅ ≥17 / ❌ <17 |
| HARDEN | `SCORE_HARDEN_BEFORE`/20 | `SCORE_HARDEN_AFTER`/20 | ✅ ≥17 / ❌ <17 |
| VALIDATE | — | `SCORE_VALIDATE_AFTER`/20 | ✅ / ❌ / SKIPPED |
SEO classique and GEO (IA) are gated independently — both must reach
≥17/20. Reaching the GEO threshold is harder than SEO classique on
many sites because AI-extraction signals (llms.txt, Speakable, QAPage,
entity SEO) are still emerging — expect more fix-loop iterations on
GEO than on SEO.
**Non-web project:**
@ -501,25 +588,43 @@ Compute final score table.
### Gate rule
Web: `ALL_PASS = (SEO_AFTER ≥ 17/20) AND (HARDEN_AFTER ≥ 17/20) AND (VALIDATE_AFTER ≥ 17/20 OR VALIDATE_SKIPPED)`
Web: `ALL_PASS = (SEO_AFTER ≥ 17/20) AND (GEO_AFTER ≥ 17/20) AND (HARDEN_AFTER ≥ 17/20) AND (VALIDATE_AFTER ≥ 17/20 OR VALIDATE_SKIPPED)`
Non-web: `ALL_PASS = (CSO_AFTER ≥ 17/20)`
**GEO gate note**: `SCORE_GEO_AFTER = "UNKNOWN"` is treated as **fail**
this typically happens when the SEO subagent produced a legacy single-score
SEO.md without the labeled `Score GEO (IA)` line. The orchestrator
re-dispatches the SEO subagent with an explicit instruction to emit both
labeled lines (see "Threshold strictness" below).
### Threshold strictness
Use the raw normalized score. **No rounding.** 16.9/20 fails. 17.0/20 passes.
A score reported as `UNKNOWN` (no parseable `Score:` line in the audit
A score reported as `UNKNOWN` (no parseable score line in the audit
file) is treated as **fail** — re-dispatch the audit subagent with an
explicit instruction to add a `Score: X/20` line at the top of its
report. Do not assume a passing score.
explicit instruction to add the score lines and re-run the audit.
Do not assume a passing score.
For `.claude/audits/SEO.md` specifically, the re-dispatch must demand
**both** labeled lines:
- `Score SEO (classique) : X.X / 20`
- `Score GEO (IA) : X.X / 20`
A single generic `Score: X/20` line is insufficient — the gate will
still mark `SCORE_GEO_AFTER = UNKNOWN` and fail.
For `.claude/audits/HARDEN.md` and `.claude/audits/CSO.md`, a single
`Score: X/20` (or `X/100`) at the top of the report is sufficient.
### Override transparency
If the user chose option C (override threshold) at any STEP 4 escalation,
write `.claude/audits/THRESHOLD-OVERRIDE.md` documenting:
- Which audit(s) were overridden
- Which audit(s) were overridden — for the SEO+GEO loop, list the axes
separately (e.g. `SEO classique: NOT overridden, GEO (IA): overridden`)
- Final score reached vs threshold
- Top 3 unresolved issues per audit
- Top 3 unresolved issues per axis
- User's stated reason
This file is referenced in §7 of the client doc ("Ce qui reste à faire ou
@ -539,7 +644,8 @@ Score table:
<the table above>
Below-threshold audits:
- SEO: <score>/20 — <top 3 remaining issues, one-line each>
- SEO (classique): <score>/20 — <top 3 remaining issues, one-line each>
- GEO (IA): <score>/20 — <top 3 remaining issues, one-line each>
- HARDEN: <score>/20 — <top 3 remaining issues>
- VALIDATE: <score>/20 — <top 3 remaining issues>
@ -561,18 +667,19 @@ Trigger: per-audit threshold violated (rule: every audit must be ≥17/20)
## Score breakdown
| Audit | Before | After | Δ | Status |
|----------|--------|-------|-----|-------------------|
| SEO | 14.4 | 16.2 | +1.8| ❌ BELOW_THRESHOLD |
| HARDEN | 12.0 | 18.0 | +6.0| ✅ OK |
| VALIDATE | — | 15.5 | — | ❌ BELOW_THRESHOLD |
| Audit | Before | After | Δ | Status |
|-------------------|--------|-------|-----|-------------------|
| SEO (classique) | 14.4 | 16.2 | +1.8| ❌ BELOW_THRESHOLD |
| GEO (IA) | 11.0 | 13.5 | +2.5| ❌ BELOW_THRESHOLD |
| HARDEN | 12.0 | 18.0 | +6.0| ✅ OK |
| VALIDATE | — | 15.5 | — | ❌ BELOW_THRESHOLD |
## Remaining issues per audit
### SEO (<score>/20)
### SEO classique (<score>/20)
[Extract from `.claude/audits/SEO.md` — the issues NOT auto-fixed.
Sort by score-gain potential. For each:]
[Extract from `.claude/audits/SEO.md` — the SEO classical issues NOT
auto-fixed. Sort by score-gain potential. For each:]
1. [TYPE] short title
- File: `path:line`
@ -580,6 +687,17 @@ Sort by score-gain potential. For each:]
- Score gain: +X.X/20
- Why automatic fix didn't work: <reason needs judgment, external account, manual content>
### GEO / IA (<score>/20)
[Extract from `.claude/audits/SEO.md` (GEO sections — §7.x) — the GEO
issues NOT auto-fixed. Same per-item format as SEO above. GEO is
gated independently at ≥17/20; below-threshold GEO blocks the handover
just like SEO classique. Common GEO blockers: missing llms.txt /
llms-full.txt, AI crawler robots.txt rules absent, no Schema.org for
AI extraction (QAPage, Speakable, HowTo, Organization graph), no
entity links (sameAs, Wikidata @id), content shape unsuited for LLM
extraction (no TL;DR, no definition lead, no Q→A blocks).]
### HARDEN (<score>/20)
... (same format)
@ -664,8 +782,9 @@ If `$ARGUMENTS` does NOT contain `--include-deploy` or `--skip-deploy`:
```
Re-grounding: project = <name>, branch = <current>, all audits passed
(web: SEO <score>/20, HARDEN <score>/20, VALIDATE <score>/20 |
non-web: CSO <score>/20). Generating client handover document.
(web: SEO classique <score>/20, GEO IA <score>/20, HARDEN <score>/20,
VALIDATE <score>/20 | non-web: CSO <score>/20). Generating client
handover document.
Le client va recevoir un document qui explique ce qui a été fait. Tu veux
qu'on ajoute aussi un chapitre qui lui explique comment construire et
@ -725,21 +844,32 @@ Tone: friendly, concrete, no jargon. One short paragraph per idea.
## 4. État de santé du site (avant / après)
[NEW SECTION — score table from STEP 8.]
[NEW SECTION — score table from STEP 8. SEO classique and GEO (IA) are
shown on separate rows so the client sees both axes explicitly.]
Avant la passe finale → après la passe finale (cette semaine) :
| Domaine | Avant | Après | Statut |
|------------------------------|-----------:|-----------:|:------:|
| Référencement Google + IA | <X.X>/20 | <Y.Y>/20 | ✅ |
| Sécurité du site | <X.X>/20 | <Y.Y>/20 | ✅ |
| Conformité technique (W3C) | — | <Z.Z>/20 | ✅ |
| Domaine | Avant | Après | Statut |
|------------------------------------------|-----------:|-----------:|:------:|
| Référencement Google (SEO classique) | <X.X>/20 | <Y.Y>/20 | ✅ |
| Visibilité IA (GEO — ChatGPT, Perplexity)| <X.X>/20 | <Y.Y>/20 | ✅ |
| Sécurité du site | <X.X>/20 | <Y.Y>/20 | ✅ |
| Conformité technique (W3C) | — | <Z.Z>/20 | ✅ |
[If LANG=en: "Site health (before / after)" with same columns.]
[If LANG=en: "Site health (before / after)" with the same columns.
Use these column labels: "Domain" / "Before" / "After" / "Status".
Row labels: "Google search (classical SEO)", "AI visibility (GEO —
ChatGPT, Perplexity)", "Site security", "Technical compliance (W3C)".]
Plain explanation under the table:
- **Référencement** = comment Google et les IA (ChatGPT, Perplexity)
trouvent et comprennent votre site.
- **Référencement Google (SEO classique)** = comment Google, Bing et
les autres moteurs traditionnels trouvent et classent votre site.
C'est ce qui amène la majorité du trafic aujourd'hui.
- **Visibilité IA (GEO)** = comment les moteurs de recherche par IA
(ChatGPT, Perplexity, Gemini, Google AI Overviews) lisent et citent
votre site. Trafic encore minoritaire mais en forte croissance —
votre site est maintenant prêt pour ce canal (llms.txt, données
structurées pour extraction IA, signaux d'entité).
- **Sécurité** = protections contre les attaques courantes (en-têtes
HTTPS, anti-injection, etc.).
- **Conformité technique** = respect des standards web (HTML, CSS,
@ -748,7 +878,7 @@ Plain explanation under the table:
[If any score had a notable jump, add a one-liner: "La sécurité est passée
de 12 à 18 — on a ajouté les en-têtes manquants et forcé le passage en
HTTPS."]
HTTPS." Do the same for SEO and GEO independently if either jumped.]
## 5. Les choix importants qu'on a faits

View File

@ -41,7 +41,7 @@ Execute the CLIENT HANDOVER WRITER agent on this project.
The agent runs a **ship-and-handover pipeline** with explicit gates:
1. **PRE-FLIGHT** — Detect git repo, project root, language, project type, web sub-type, NAP signals, stack.
2. **BASELINE AUDITS** — Run /seo (SEO+GEO) and /harden in parallel. Capture initial scores (`SCORE_SEO_BEFORE`, `SCORE_HARDEN_BEFORE`).
2. **BASELINE AUDITS** — Run /seo (SEO+GEO) and /harden in parallel. Capture initial scores (`SCORE_SEO_BEFORE`, `SCORE_GEO_BEFORE`, `SCORE_HARDEN_BEFORE`).
3. **FIX LOOPS (parallel, bounded)** — For each audit < 17/20:
- Re-invoke the audit subagent with explicit instruction to apply auto-fixes.
- Re-score.
@ -50,9 +50,9 @@ The agent runs a **ship-and-handover pipeline** with explicit gates:
4. **COMMIT + PUSH** — If files changed during fix loops, run /commit-change (atomic logical commits) then `git push`.
5. **DEPLOY PAUSE** — List exact deploy artifacts: changed files since baseline, deploy hints from project (vercel.json, netlify.toml, Dockerfile, .github/workflows/deploy.yml, etc.), and the deploy process in plain words. Use AskUserQuestion: "Deploy done? (Yes / Not yet / Skip validate)". Block until Yes or Skip.
6. **/validate (live site)** — Run validator-analyzer against the deployed URL. Capture `SCORE_VALIDATE`.
7. **GATE — per-audit threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO, HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold audit. Do NOT write the client deliverable. Report to user.
7. **GATE — per-axis threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO classique, GEO (IA), HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold axis. Do NOT write the client deliverable. Report to user.
8. **DOC GENERATION (only if all scores ≥17/20)** — Read `.claude/memory/` registries + full git history. Ask whether to include build/deploy chapter. Synthesize concise client deliverable with:
- Before/after score table (SEO, HARDEN, VALIDATE — values + delta).
- Before/after score table with SEO classique and GEO (IA) on separate rows, plus HARDEN and VALIDATE — values + delta. SEO classique, GEO, HARDEN and VALIDATE are gated independently — each must reach ≥17/20 for the pipeline to pass.
- Plain-language summary of all changes since first commit.
- **Owner responsibilities** section: explicit checklist of what the client must do / maintain (SEO platforms, content updates, monitoring, deploy if self-hosted).
- Optional build/deploy chapter.