SERP Content Teardown
Deterministic (no-LLM) pipeline that reverse-engineers what content wins a Google / AI-search niche, from local Semrush xlsx exports + fetched competitor HTML. It answers, per target topic: which article archetype to write, how long, how many H2s, what schema + FAQ, what opening/closing pattern, which keywords to target, what backlink authority is realistically needed, and what GEO (AI-Overview) posture to take.
Everything runs offline against files you already have, except one network step: fetching competitor article HTML via curl with a browser UA. No paid APIs, no live AI-citation probing (see references/honest-scope.md).
Quick Start
cd brand-strategy/serp-content-teardown
# One-shot: runs all 8 steps in order, writes every artifact to --out-dir.
python3 scripts/run_all.py \
--semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \
--out-dir ./teardown_out \
--topics templates/topic-clusters.yaml \
--brand-names templates/brand-names.json \
--top 30
run_all.py is just the per-step scripts chained. Run them individually when iterating (e.g. re-fetch without re-parsing, or re-classify after tweaking the topic map):
# 1. Parse Semrush serp_urls xlsx -> ranked blog/info-article URL pool
python3 scripts/parse_serp.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \
--out-dir ./teardown_out --topics templates/topic-clusters.yaml --limit 40
# 2. Fetch top competitor articles (curl, browser UA). --select to override the auto-pick.
python3 scripts/fetch_competitors.py --out-dir ./teardown_out --top 30
# 3. Per-article structure metrics -> results.json + prose_dump.txt
python3 scripts/analyze_structure.py --out-dir ./teardown_out --brand-names templates/brand-names.json
# 4. Classify into 8 archetypes + opening/closing patterns + aggregate bands
python3 scripts/classify_archetypes.py --out-dir ./teardown_out
# 5. Keyword distribution + core keywords + SERP-feature triggers (broad-match xlsx)
python3 scripts/keyword_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \
--out-dir ./teardown_out --topics templates/topic-clusters.yaml
# 6. Backlink / authority thresholds + weak-link winners (serp_urls Organic rows)
python3 scripts/backlink_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \
--out-dir ./teardown_out --topics templates/topic-clusters.yaml
# 7. GEO: AI-Overview saturation + cited domains + schema readiness of cited vs non-cited
python3 scripts/geo_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \
--out-dir ./teardown_out
# 8. On-page SEO: title/meta/H1/canonical/internal-link density + SERP features
python3 scripts/onpage_analysis.py --out-dir ./teardown_out
Steps 3, 4, 7, 8 reuse the HTML fetched in step 2 — no re-fetching. Steps 1, 5, 6, 7 read the Semrush xlsx directly.
Inputs
| Input |
Required |
What it is |
Where |
*serp_urls*.xlsx |
yes |
Semrush "SERP URLs" export per keyword/region. Columns used: URL, Position, Type (Organic / AI Overview / …), Search Traffic, Page AS, Ref.Domains, Backlinks. Drives the URL pool (step 1), authority thresholds (step 6), AI-cited domains (step 7). |
--semrush-dir |
*broad-match*.xlsx |
for steps 5/7 |
Semrush "Broad Match" keyword export (US). Columns used: Keyword, Volume, Keyword Difficulty, Intent, SERP Features. Drives keyword distribution + GEO sizing. |
--semrush-dir |
templates/topic-clusters.yaml |
yes |
Ordered cluster → regex map (first match wins as the keyword's primary cluster). Also seeds the blog/info-article path heuristics. Adapt to your niche. |
--topics |
templates/brand-names.json |
optional |
domain → [name variants] for brand-mention density. If a domain is missing, the brand token is auto-derived from the domain (e.g. oceanwavejewelry.com → oceanwave); use specific forms only, never generic words. |
--brand-names |
| fetched HTML |
produced |
fetch_competitors.py writes html/<slug>.html + fetch_manifest.json; steps 3/4/7/8 re-parse these. |
--out-dir/html/ |
The Semrush directory layout mirrors tools/backlink-kol-extractor — point --semrush-dir at the folder holding the *serp_urls* and *broad-match* xlsx files (typically ~/Downloads/semrush/_project_<niche>/_keywords).
Output schema
All artifacts land under --out-dir. Full field tables in references/output-schema.md.
| Artifact |
Produced by |
Contents |
url_pool.json |
parse_serp |
Ranked blog/info-article URLs: domain, path, url, category (blog / info_page), appearances, matched_keywords, us_min_pos, all_min_pos, avg_pos, n_keywords, max_traffic. |
html/, fetch_manifest.json |
fetch_competitors |
Raw HTML per article + fetch log (http_code, bytes, ok, anti-bot block flag). |
results.json |
analyze_structure |
Per-article structure metrics (word_count, h1/h2/h3, imgs, ul/ol/table, schema_types, schema_blocks, has_faq_section, author, dates, brand_per_1k, authority_outlinks, h2_texts, opening, closing). |
prose_dump.txt |
analyze_structure |
Human-readable opening + H2 outline + closing per article (for eyeballing tone). |
classified.json |
classify_archetypes |
results.json + archetype, archetype_reason, opening_pattern, closing_pattern, schema_sig. |
keywords.json |
keyword_analysis |
Deduped keyword universe: kw, vol, kd, intent, feats, cluster. Console prints intent/volume/KD/cluster distributions + core + quick-win lists. |
backlinks.json |
backlink_analysis |
Per organic SERP row: kw, cluster, pos, as (Page AS), ref (Ref.Domains), bl (Backlinks), domain, url. Console prints authority bands + weak-link winners. |
onpage.json |
onpage_analysis |
Per-article on-page: title/meta/H1, canonical, kw-in-title/h1/meta, title_is_question, internal/external link counts. |
The content-strategy report (per-topic recommendations) is assembled from these artifacts — see templates/ and examples/ (owned by the report templates) for the report shape; the methodology for reading them is in references/methodology.md.
When to use
- You have Semrush exports for a niche and want to know what to actually write — archetype, length, structure, schema — not just a keyword list.
- New informational-SEO content cluster: reverse-engineer the incumbents before drafting.
- You want a defensible authority read: "is the long tail beatable with content alone, or do I need links first?"
- You want to size the GEO / AI-Overview opportunity per topic from data you already pay Semrush for.
When NOT to use
- KOL / influencer / media prospect discovery → use
tools/backlink-kol-extractor (different anchor: refdomains, not article structure).
- Implementing the schema the teardown recommends → use
structured-data-buildout (this skill measures schema; it does not write it).
- You have no Semrush data → out of scope. This pipeline is built around Semrush xlsx column shapes; it does not crawl a SERP live or call a keyword API.
- You want a domain-authority (DR) verdict → Semrush exports carry page-level Authority Score, not domain DR. See honest-scope.
- Cloudflare-heavy targets → the fetcher is
curl-only; pages behind a JS challenge get flagged blocked and skipped (no headless browser, no paid scraper). Expect to lose a few of the top URLs.
Honest scope
This finds the structure / SEO / GEO "code-side" of what wins — roughly the 20-30% you can copy mechanically. The other 70-80% is content quality, domain age, and backlinks, none of which this measures or fixes. Two specific limits worth stating up front to the user:
- Backlink data is Semrush Page Authority Score (page-level), not domain DR. You can confidently say "page-level links aren't the bottleneck for the long tail" (the data shows top-3 pages routinely ranking at Page AS 0-20); you cannot quantify the domain-authority floor from this.
- AI-Overview citation capture in Semrush exports is sparse. "Schema depth didn't gate citation in-sample" is what the data supports — not "schema is useless." Flag any GEO estimate with ⚠️.
Full caveats + the red line on paid APIs / live AI probing: references/honest-scope.md.
References
- references/methodology.md — the full 8-step pipeline (input → deterministic algorithm → output) + how to read the cross-sample findings.
- references/archetypes.md — the 8-archetype spec: trigger rule, word/H2 bands, FAQ, schema set, opening/closing pattern, "when it wins".
- references/honest-scope.md — what this does and does NOT tell you; the paid-API / AI-probe red line; parser gotchas.
- references/output-schema.md — JSON field schemas for every artifact.
1---2name: serp-content-teardown3description: SERP Content Teardown4---56# SERP Content Teardown78Deterministic (no-LLM) pipeline that reverse-engineers what content wins a Google / AI-search niche, from **local Semrush xlsx exports + fetched competitor HTML**. It answers, per target topic: which article archetype to write, how long, how many H2s, what schema + FAQ, what opening/closing pattern, which keywords to target, what backlink authority is realistically needed, and what GEO (AI-Overview) posture to take.910Everything runs offline against files you already have, except one network step: fetching competitor article HTML via `curl` with a browser UA. **No paid APIs, no live AI-citation probing** (see [references/honest-scope.md](references/honest-scope.md)).1112## Quick Start1314```bash15cd brand-strategy/serp-content-teardown1617# One-shot: runs all 8 steps in order, writes every artifact to --out-dir.18python3 scripts/run_all.py \19 --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \20 --out-dir ./teardown_out \21 --topics templates/topic-clusters.yaml \22 --brand-names templates/brand-names.json \23 --top 3024```2526`run_all.py` is just the per-step scripts chained. Run them individually when iterating (e.g. re-fetch without re-parsing, or re-classify after tweaking the topic map):2728```bash29# 1. Parse Semrush serp_urls xlsx -> ranked blog/info-article URL pool30python3 scripts/parse_serp.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \31 --out-dir ./teardown_out --topics templates/topic-clusters.yaml --limit 403233# 2. Fetch top competitor articles (curl, browser UA). --select to override the auto-pick.34python3 scripts/fetch_competitors.py --out-dir ./teardown_out --top 303536# 3. Per-article structure metrics -> results.json + prose_dump.txt37python3 scripts/analyze_structure.py --out-dir ./teardown_out --brand-names templates/brand-names.json3839# 4. Classify into 8 archetypes + opening/closing patterns + aggregate bands40python3 scripts/classify_archetypes.py --out-dir ./teardown_out4142# 5. Keyword distribution + core keywords + SERP-feature triggers (broad-match xlsx)43python3 scripts/keyword_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \44 --out-dir ./teardown_out --topics templates/topic-clusters.yaml4546# 6. Backlink / authority thresholds + weak-link winners (serp_urls Organic rows)47python3 scripts/backlink_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \48 --out-dir ./teardown_out --topics templates/topic-clusters.yaml4950# 7. GEO: AI-Overview saturation + cited domains + schema readiness of cited vs non-cited51python3 scripts/geo_analysis.py --semrush-dir ~/Downloads/semrush/_project_<niche>/_keywords \52 --out-dir ./teardown_out5354# 8. On-page SEO: title/meta/H1/canonical/internal-link density + SERP features55python3 scripts/onpage_analysis.py --out-dir ./teardown_out56```5758Steps 3, 4, 7, 8 reuse the HTML fetched in step 2 — no re-fetching. Steps 1, 5, 6, 7 read the Semrush xlsx directly.5960## Inputs6162| Input | Required | What it is | Where |63|---|---|---|---|64| `*serp_urls*.xlsx` | yes | Semrush "SERP URLs" export per keyword/region. Columns used: `URL`, `Position`, `Type` (Organic / AI Overview / …), `Search Traffic`, `Page AS`, `Ref.Domains`, `Backlinks`. Drives the URL pool (step 1), authority thresholds (step 6), AI-cited domains (step 7). | `--semrush-dir` |65| `*broad-match*.xlsx` | for steps 5/7 | Semrush "Broad Match" keyword export (US). Columns used: `Keyword`, `Volume`, `Keyword Difficulty`, `Intent`, `SERP Features`. Drives keyword distribution + GEO sizing. | `--semrush-dir` |66| `templates/topic-clusters.yaml` | yes | Ordered cluster → regex map (first match wins as the keyword's primary cluster). Also seeds the blog/info-article path heuristics. Adapt to your niche. | `--topics` |67| `templates/brand-names.json` | optional | `domain → [name variants]` for brand-mention density. If a domain is missing, the brand token is auto-derived from the domain (e.g. `oceanwavejewelry.com` → `oceanwave`); use specific forms only, never generic words. | `--brand-names` |68| fetched HTML | produced | `fetch_competitors.py` writes `html/<slug>.html` + `fetch_manifest.json`; steps 3/4/7/8 re-parse these. | `--out-dir/html/` |6970The Semrush directory layout mirrors `tools/backlink-kol-extractor` — point `--semrush-dir` at the folder holding the `*serp_urls*` and `*broad-match*` xlsx files (typically `~/Downloads/semrush/_project_<niche>/_keywords`).7172## Output schema7374All artifacts land under `--out-dir`. Full field tables in [references/output-schema.md](references/output-schema.md).7576| Artifact | Produced by | Contents |77|---|---|---|78| `url_pool.json` | parse_serp | Ranked blog/info-article URLs: `domain, path, url, category` (blog / info_page), `appearances`, `matched_keywords`, `us_min_pos`, `all_min_pos`, `avg_pos`, `n_keywords`, `max_traffic`. |79| `html/`, `fetch_manifest.json` | fetch_competitors | Raw HTML per article + fetch log (`http_code`, `bytes`, `ok`, anti-bot block flag). |80| `results.json` | analyze_structure | Per-article structure metrics (word_count, h1/h2/h3, imgs, ul/ol/table, schema_types, schema_blocks, has_faq_section, author, dates, brand_per_1k, authority_outlinks, h2_texts, opening, closing). |81| `prose_dump.txt` | analyze_structure | Human-readable opening + H2 outline + closing per article (for eyeballing tone). |82| `classified.json` | classify_archetypes | `results.json` + `archetype`, `archetype_reason`, `opening_pattern`, `closing_pattern`, `schema_sig`. |83| `keywords.json` | keyword_analysis | Deduped keyword universe: `kw, vol, kd, intent, feats, cluster`. Console prints intent/volume/KD/cluster distributions + core + quick-win lists. |84| `backlinks.json` | backlink_analysis | Per organic SERP row: `kw, cluster, pos, as (Page AS), ref (Ref.Domains), bl (Backlinks), domain, url`. Console prints authority bands + weak-link winners. |85| `onpage.json` | onpage_analysis | Per-article on-page: title/meta/H1, canonical, kw-in-title/h1/meta, title_is_question, internal/external link counts. |8687The **content-strategy report** (per-topic recommendations) is assembled from these artifacts — see `templates/` and `examples/` (owned by the report templates) for the report shape; the methodology for reading them is in [references/methodology.md](references/methodology.md).8889## When to use9091- You have Semrush exports for a niche and want to know **what to actually write** — archetype, length, structure, schema — not just a keyword list.92- New informational-SEO content cluster: reverse-engineer the incumbents before drafting.93- You want a defensible authority read: "is the long tail beatable with content alone, or do I need links first?"94- You want to size the GEO / AI-Overview opportunity per topic from data you already pay Semrush for.9596## When NOT to use9798- **KOL / influencer / media prospect discovery** → use `tools/backlink-kol-extractor` (different anchor: refdomains, not article structure).99- **Implementing the schema** the teardown recommends → use `structured-data-buildout` (this skill measures schema; it does not write it).100- **You have no Semrush data** → out of scope. This pipeline is built around Semrush xlsx column shapes; it does not crawl a SERP live or call a keyword API.101- **You want a domain-authority (DR) verdict** → Semrush exports carry *page-level* Authority Score, not domain DR. See honest-scope.102- **Cloudflare-heavy targets** → the fetcher is `curl`-only; pages behind a JS challenge get flagged `blocked` and skipped (no headless browser, no paid scraper). Expect to lose a few of the top URLs.103104## Honest scope105106This finds the **structure / SEO / GEO "code-side" of what wins — roughly the 20-30%** you can copy mechanically. The other **70-80% is content quality, domain age, and backlinks**, none of which this measures or fixes. Two specific limits worth stating up front to the user:107108- Backlink data is Semrush **Page Authority Score (page-level), not domain DR**. You can confidently say "page-level links aren't the bottleneck for the long tail" (the data shows top-3 pages routinely ranking at Page AS 0-20); you **cannot** quantify the domain-authority floor from this.109- AI-Overview citation capture in Semrush exports is **sparse**. "Schema depth didn't gate citation in-sample" is what the data supports — *not* "schema is useless." Flag any GEO estimate with ⚠️.110111Full caveats + the red line on paid APIs / live AI probing: [references/honest-scope.md](references/honest-scope.md).112113## References114115- [references/methodology.md](references/methodology.md) — the full 8-step pipeline (input → deterministic algorithm → output) + how to read the cross-sample findings.116- [references/archetypes.md](references/archetypes.md) — the 8-archetype spec: trigger rule, word/H2 bands, FAQ, schema set, opening/closing pattern, "when it wins".117- [references/honest-scope.md](references/honest-scope.md) — what this does and does NOT tell you; the paid-API / AI-probe red line; parser gotchas.118- [references/output-schema.md](references/output-schema.md) — JSON field schemas for every artifact.