Mirroring Competitor Codebases (the Codebase Mirror)
Overview
When a competitor outranks you, the instinct is to read their copy. But a lot of what search and answer engines reward is not written by a writer at all — it is emitted by a template: the JSON-LD block, the heading shape, the internal-link footer, whether the page arrives as HTML or as an empty <div id="root">. Those are codebase decisions, and they are visible in the HTML they serve.
The Codebase Mirror reads that HTML on both sides and answers one question: what does their template do that yours doesn't?
It reports gaps in one direction only. A pattern you ship and they don't is not in the report — this list exists to be acted on, and acting on it means adding what is missing.
Where the line is with analyzing-content-gaps
They are complementary and easy to confuse:
|
analyzing-content-gaps |
mirroring-competitor-codebases |
| Unit |
One query, one page vs the top 3 |
Whole site, 3–5 comparable page types per side |
| Reads |
Prose — entities, sub-questions, depth |
Implementation — what the template emits |
| Output |
CONTENT_BRIEF.md — what to write |
Ranked gap list — what to build |
| Judgment |
LLM reads and compares meaning |
Deterministic thresholds, no judgment at runtime |
Rule of thumb: "what should this page say?" → content gaps. "what should our template emit?" → this skill. Run both when a competitor beats you on a query you care about; the fixes land in different PRs and usually different files.
Architecture: where the pieces live
| Piece |
Lives in |
Makes live calls? |
| Page fetcher |
scripts/codebase-mirror.js (loadPages) |
Yes — plain HTTP GETs, robots.txt-gated for our user-agent, one request per origin per --delay ms. Local HTML files work too. |
| Signal extraction + gap engine + ranking |
scripts/codebase-mirror.js |
No — pure, offline, deterministic, fully unit-tested. |
| Which pages to compare |
You (or analyzing-content-gaps for the SERP) |
The tool mirrors the URLs you hand it. It does not crawl. |
| SERP positions for ranking |
GSC, via finding-underserved-keywords |
Passed in with --serp. Absent → severity-only ranking, tagged serp: unknown. |
Everything the engine does is unit-tested against fixtures (test/codebase-mirror.test.js, HTML in fixtures/codebase-mirror/ours/home.html and fixtures/codebase-mirror/theirs/home.html).
The loop
Pick comparable page types. 3–5 per side, matched by role, not by URL: their home vs your home, their money page vs your money page, two of their guide/blog pages vs two of yours. Mirroring their blog against your pricing page produces noise, and one page per side produces anecdotes — the tool warns when a side has fewer than two.
Get the SERP positions (optional but this is what makes the ranking mean something). From GSC via finding-underserved-keywords: your average position for the query, and theirs from the live SERP. Write them into a small JSON file:
{ "query": "invoicing software for freelancers",
"ours": { "url": "https://ledgerly.example/", "position": 14.2 },
"theirs": { "url": "https://billfold.example/", "position": 3.1 } }
Run the mirror:
node scripts/codebase-mirror.js \
--ours https://yoursite.com/ \
--ours https://yoursite.com/pricing \
--ours https://yoursite.com/guide/setup \
--theirs https://competitor.com/ \
--theirs https://competitor.com/pricing \
--theirs https://competitor.com/guide/setup \
--serp .claude/seo/serp-positions.json \
--json .claude/seo/codebase-mirror.json
Try it with no setup at all against the shipped fixtures:
node scripts/codebase-mirror.js \
--ours fixtures/codebase-mirror/ours/home.html \
--theirs fixtures/codebase-mirror/theirs/home.html
Read the ranked list. Each row is severity × SERP multiplier = impact, with the two rates that produced it (them 100% / you 0%) and a hand-off to the skill that fixes that class of gap.
Act, top-down, one PR per axis. Schema gaps → adding-schema-markup. Rendering → auditing-technical-seo. Heading shape → optimizing-for-generative-engines. Internal links → planning-topic-clusters. Meta coverage → optimizing-on-page or seo-bootstrap. Then re-run the mirror; the closed rows should disappear.
What it looks at, and why each axis is there
| Axis |
Signal |
Why it is a codebase gap, not a copy gap |
| Rendering |
Body words present in the delivered HTML vs an empty shell + scripts |
AI-search crawlers do not execute your JS. A client-shell page is invisible to them no matter how good the copy is. Highest severity for a reason. |
| Schema |
Page-level JSON-LD @types, per-page rate |
A type on ≥50% of their pages is a template, not a one-off. Nested children (Question inside FAQPage) are deliberately not counted — that would report one gap twice. |
| Heading shape |
Share of H2/H3 phrased as questions |
The question-then-direct-answer shape is what AI Overviews lift. It is a component decision as often as a writing one. |
| Internal links |
Median internal links per page, descriptive vs generic anchors |
Link density is emitted by layout: related-posts blocks, footers, breadcrumbs. |
| Meta coverage |
description, canonical, OG title+image, hreflang, image alt |
Coverage rates expose which template forgot the field, not which page. |
| Freshness |
dateModified present in schema |
Pages updated <3 months out are cited roughly twice as often by AI engines. |
| Depth |
Median word count |
Reported last and lowest on purpose — adding words without the axes above is the skyscraper trap. |
Every threshold is a named constant at the top of scripts/codebase-mirror.js. If you disagree with one, change the constant — do not argue with the output.
Reading the ranking honestly
- With
--serp: the multiplier encodes how much their pattern is worth as evidence. They beat you by 10+ positions → ×2.0. They edge you out → ×1.2. You already outrank them → ×0.75 — their pattern is weak evidence, and copying a worse-performing site is how you talk yourself into work that does not pay.
- Without
--serp: every row is tagged serp: "unknown" and ranked by severity alone. The tool never invents a position, and the report says so in a warning rather than quietly looking SERP-informed.
- Correlation, not causation. "They rank above you and their template does X" is not proof X is why. It is the strongest cheap evidence available — treat the high-severity rows as hypotheses worth a PR, not as a diagnosis.
Etiquette and legality
- It reads the HTML any crawler receives, at crawler volume: robots.txt is honoured for our user-agent, and a disallowed path is skipped with a warning rather than fetched anyway. One request per origin per second by default (
--delay).
- It never touches a competitor's repo, admin, or private endpoints — "codebase mirror" means the shipped implementation, not their source.
- Copy patterns, never content. Emitting
FAQPage because they do is fine. Copying their FAQ answers is duplicate content that Google flags and AI systems deprioritize. The report deliberately gives you rates and structures, not their prose.
What this is NOT
- Not a crawler. It mirrors the URLs you hand it. Site-wide discovery is out of scope by design — a crawl of someone else's site is a different, heavier, ruder tool.
- Not a renderer. It sees what arrives in the HTML. Content injected client-side is invisible here — exactly as it is to an AI-search crawler, which is why
rendering is an axis rather than a caveat.
- Not a content brief. No entities, no sub-question analysis, no writing guidance. That is
analyzing-content-gaps.
- Not a verdict on their ranking. It explains what they ship, not why Google likes them.
Common mistakes
- Mirroring one page per side. Rates over one page are anecdotes; the tool warns, and the warning is real. Three to five comparable pages per side.
- Comparing unlike page types. Their blog against your landing page manufactures gaps that mean nothing.
- Running it without
--serp, then treating the order as truth. Severity-only ordering is a reasonable default and an explicitly labelled one. Get the positions if the ranking is going to drive the sprint.
- Starting with the depth row. It is last for a reason. Close schema/rendering/heading gaps, then re-measure — depth usually shrinks on its own once the structure is right.
- Copying a pattern from a site you already outrank. Check the multiplier before you build.
Lifecycle awareness
Growth → Mature. Needs a live competitor and a live you. Pre-launch there is nothing to mirror on your side — use researching-keywords-pre-launch and seo-bootstrap first, then come back once your pages exist.
What next
- Schema rows →
adding-schema-markup.
- Rendering row →
auditing-technical-seo.
- Question-heading row →
optimizing-for-generative-engines.
- Internal-link row →
planning-topic-clusters.
- Meta rows →
optimizing-on-page (per page) or seo-bootstrap (site-wide defaults).
- Depth row, last →
analyzing-content-gaps for what to actually add.
1---2name: mirroring-competitor-codebases3description: Use when comparing your site's *implementation* against a competitor's — "what is their site doing that mine isn't", "mirror their codebase", "reverse-engineer their SEO setup", "why is their markup better", "competitor schema/heading/internal-link audit", "they rank above me, what are they shipping". Runs the Competitor Codebase Mirror — parses the HTML both sites actually serve, folds each side into per-page rates (schema types, question-heading shape, internal-link density, meta coverage, server-rendered vs client shell), and reports only the patterns they ship that you don't, ranked by SERP delta. Template-level and site-wide, not prose-level for one query — for the per-query content diff use `analyzing-content-gaps`.4---56# Mirroring Competitor Codebases (the Codebase Mirror)78## Overview910When a competitor outranks you, the instinct is to read their copy. But a lot of what search and answer engines reward is not written by a writer at all — it is emitted by a template: the JSON-LD block, the heading shape, the internal-link footer, whether the page arrives as HTML or as an empty `<div id="root">`. Those are **codebase decisions**, and they are visible in the HTML they serve.1112The Codebase Mirror reads that HTML on both sides and answers one question: **what does their template do that yours doesn't?**1314It reports gaps in one direction only. A pattern *you* ship and they don't is not in the report — this list exists to be acted on, and acting on it means adding what is missing.1516## Where the line is with `analyzing-content-gaps`1718They are complementary and easy to confuse:1920| | `analyzing-content-gaps` | `mirroring-competitor-codebases` |21|---|---|---|22| Unit | One query, one page vs the top 3 | Whole site, 3–5 comparable page types per side |23| Reads | Prose — entities, sub-questions, depth | Implementation — what the template emits |24| Output | `CONTENT_BRIEF.md` — what to write | Ranked gap list — what to build |25| Judgment | LLM reads and compares meaning | Deterministic thresholds, no judgment at runtime |2627Rule of thumb: **"what should this page say?" → content gaps. "what should our template emit?" → this skill.** Run both when a competitor beats you on a query you care about; the fixes land in different PRs and usually different files.2829## Architecture: where the pieces live3031| Piece | Lives in | Makes live calls? |32|---|---|---|33| Page fetcher | `scripts/codebase-mirror.js` (`loadPages`) | Yes — plain HTTP GETs, robots.txt-gated for our user-agent, one request per origin per `--delay` ms. Local HTML files work too. |34| Signal extraction + gap engine + ranking | `scripts/codebase-mirror.js` | **No** — pure, offline, deterministic, fully unit-tested. |35| Which pages to compare | You (or `analyzing-content-gaps` for the SERP) | The tool mirrors the URLs you hand it. It does not crawl. |36| SERP positions for ranking | GSC, via `finding-underserved-keywords` | Passed in with `--serp`. Absent → severity-only ranking, tagged `serp: unknown`. |3738Everything the engine does is unit-tested against fixtures (`test/codebase-mirror.test.js`, HTML in `fixtures/codebase-mirror/ours/home.html` and `fixtures/codebase-mirror/theirs/home.html`).3940## The loop41421. **Pick comparable page types.** 3–5 per side, matched by *role*, not by URL: their home vs your home, their money page vs your money page, two of their guide/blog pages vs two of yours. Mirroring their blog against your pricing page produces noise, and one page per side produces anecdotes — the tool warns when a side has fewer than two.43442. **Get the SERP positions** (optional but this is what makes the ranking mean something). From GSC via `finding-underserved-keywords`: your average position for the query, and theirs from the live SERP. Write them into a small JSON file:45 ```json46 { "query": "invoicing software for freelancers",47 "ours": { "url": "https://ledgerly.example/", "position": 14.2 },48 "theirs": { "url": "https://billfold.example/", "position": 3.1 } }49 ```50513. **Run the mirror:**52 ```bash53 node scripts/codebase-mirror.js \54 --ours https://yoursite.com/ \55 --ours https://yoursite.com/pricing \56 --ours https://yoursite.com/guide/setup \57 --theirs https://competitor.com/ \58 --theirs https://competitor.com/pricing \59 --theirs https://competitor.com/guide/setup \60 --serp .claude/seo/serp-positions.json \61 --json .claude/seo/codebase-mirror.json62 ```63 Try it with no setup at all against the shipped fixtures:64 ```bash65 node scripts/codebase-mirror.js \66 --ours fixtures/codebase-mirror/ours/home.html \67 --theirs fixtures/codebase-mirror/theirs/home.html68 ```69704. **Read the ranked list.** Each row is `severity × SERP multiplier = impact`, with the two rates that produced it (`them 100% / you 0%`) and a hand-off to the skill that fixes that class of gap.71725. **Act, top-down, one PR per axis.** Schema gaps → `adding-schema-markup`. Rendering → `auditing-technical-seo`. Heading shape → `optimizing-for-generative-engines`. Internal links → `planning-topic-clusters`. Meta coverage → `optimizing-on-page` or `seo-bootstrap`. Then re-run the mirror; the closed rows should disappear.7374## What it looks at, and why each axis is there7576| Axis | Signal | Why it is a codebase gap, not a copy gap |77|---|---|---|78| **Rendering** | Body words present in the delivered HTML vs an empty shell + scripts | AI-search crawlers do not execute your JS. A client-shell page is invisible to them no matter how good the copy is. Highest severity for a reason. |79| **Schema** | Page-level JSON-LD `@type`s, per-page rate | A type on ≥50% of their pages is a *template*, not a one-off. Nested children (`Question` inside `FAQPage`) are deliberately not counted — that would report one gap twice. |80| **Heading shape** | Share of H2/H3 phrased as questions | The question-then-direct-answer shape is what AI Overviews lift. It is a component decision as often as a writing one. |81| **Internal links** | Median internal links per page, descriptive vs generic anchors | Link density is emitted by layout: related-posts blocks, footers, breadcrumbs. |82| **Meta coverage** | description, canonical, OG title+image, hreflang, image `alt` | Coverage *rates* expose which template forgot the field, not which page. |83| **Freshness** | `dateModified` present in schema | Pages updated <3 months out are cited roughly twice as often by AI engines. |84| **Depth** | Median word count | Reported **last and lowest on purpose** — adding words without the axes above is the skyscraper trap. |8586Every threshold is a named constant at the top of `scripts/codebase-mirror.js`. If you disagree with one, change the constant — do not argue with the output.8788## Reading the ranking honestly8990- **With `--serp`:** the multiplier encodes how much their pattern is worth as evidence. They beat you by 10+ positions → ×2.0. They edge you out → ×1.2. **You already outrank them → ×0.75** — their pattern is weak evidence, and copying a worse-performing site is how you talk yourself into work that does not pay.91- **Without `--serp`:** every row is tagged `serp: "unknown"` and ranked by severity alone. The tool never invents a position, and the report says so in a warning rather than quietly looking SERP-informed.92- **Correlation, not causation.** "They rank above you and their template does X" is not proof X is why. It is the strongest cheap evidence available — treat the high-severity rows as hypotheses worth a PR, not as a diagnosis.9394## Etiquette and legality9596- It reads the HTML any crawler receives, at crawler volume: **robots.txt is honoured** for our user-agent, and a disallowed path is skipped with a warning rather than fetched anyway. One request per origin per second by default (`--delay`).97- It never touches a competitor's repo, admin, or private endpoints — "codebase mirror" means the shipped implementation, not their source.98- **Copy patterns, never content.** Emitting `FAQPage` because they do is fine. Copying their FAQ answers is duplicate content that Google flags and AI systems deprioritize. The report deliberately gives you rates and structures, not their prose.99100## What this is NOT101102- **Not a crawler.** It mirrors the URLs you hand it. Site-wide discovery is out of scope by design — a crawl of someone else's site is a different, heavier, ruder tool.103- **Not a renderer.** It sees what arrives in the HTML. Content injected client-side is invisible here — exactly as it is to an AI-search crawler, which is why `rendering` is an axis rather than a caveat.104- **Not a content brief.** No entities, no sub-question analysis, no writing guidance. That is `analyzing-content-gaps`.105- **Not a verdict on their ranking.** It explains what they ship, not why Google likes them.106107## Common mistakes108109- **Mirroring one page per side.** Rates over one page are anecdotes; the tool warns, and the warning is real. Three to five comparable pages per side.110- **Comparing unlike page types.** Their blog against your landing page manufactures gaps that mean nothing.111- **Running it without `--serp`, then treating the order as truth.** Severity-only ordering is a reasonable default and an explicitly labelled one. Get the positions if the ranking is going to drive the sprint.112- **Starting with the depth row.** It is last for a reason. Close schema/rendering/heading gaps, then re-measure — depth usually shrinks on its own once the structure is right.113- **Copying a pattern from a site you already outrank.** Check the multiplier before you build.114115## Lifecycle awareness116117Growth → Mature. Needs a live competitor and a live you. Pre-launch there is nothing to mirror on your side — use `researching-keywords-pre-launch` and `seo-bootstrap` first, then come back once your pages exist.118119## What next120121- **Schema rows →** `adding-schema-markup`.122- **Rendering row →** `auditing-technical-seo`.123- **Question-heading row →** `optimizing-for-generative-engines`.124- **Internal-link row →** `planning-topic-clusters`.125- **Meta rows →** `optimizing-on-page` (per page) or `seo-bootstrap` (site-wide defaults).126- **Depth row, last →** `analyzing-content-gaps` for what to actually add.