# SEO.md — SEO & Discoverability Audit

**Date:** 2026-07-09
**Auditor:** SEO & discoverability brief (70), read-only pass
**Prior run:** none — no previous `SEO.md` existed; this is the first SEO audit.
**Method note:** live fetches of goal-prompts.vercel.app were not possible from this
environment (outbound blocked), so this audit was run against the built artifacts in
the repo — `index.html`, `b/`, `p/`, `examples/`, `sitemap.xml`, `robots.txt`, `raw/`,
`sw.js` — which are regenerated by `build.py` and match HEAD (clean tree), i.e. they are
byte-for-byte what Vercel deploys. Routing behavior (`cleanUrls`, redirects, headers)
was read from `vercel.json`.

---

## 1 · Verdict

**Indexable and shareable today: yes.** This is a fully static site with real SEO
machinery, actually used: 180 URLs in `sitemap.xml`, a correct `robots.txt`, unique
per-page `<title>` + meta description + canonical on every brief and playbook page
(emitted by `page()` in `build.py:758–787`), per-brief 1200×630 OG images (`og/*.png`,
141 files, verified dimensions), no `noindex` anywhere, one `h1` per page, no
query-param duplicates (all catalog state lives in the URL hash), and `cleanUrls: true`
in `vercel.json` collapsing `.html` variants. The 141 `b/` pages are exemplary: full
content in static HTML including the verbatim brief text, dense static cross-links
("pairs well with", family siblings, playbook links).

**The single biggest leak:** the homepage's catalog — the site's whole reason to
rank — does not exist in its static HTML. The 141 brief cards and 35 playbook cards
are rendered client-side from a ~507 KB inline `const DATA` blob (`index.html:765`,
render code around `index.html:966–1052`). Outside `<script>` tags, `index.html`
contains exactly **one** link to a brief page (`/b/46`) and **one** to a playbook
(`/p/codereview-collab`). Non-rendering crawlers (most non-Google bots, LLM/AI-answer
crawlers, social scrapers) see a homepage with no path to 176 detail pages, and even
for Google the homepage passes almost no static link equity to them — discovery routes
entirely through the sitemap. The same blob makes the homepage a 572 KB HTML document
(~165 KB gzipped), the site's only real Core Web Vitals liability.

---

## 2 · Three page checks

| Page | What the crawler sees | What the unfurl renders | Gap |
|---|---|---|---|
| `/` (`index.html`) | Full head (unique title, description, canonical, SoftwareApplication JSON-LD at line 20), hero `h1`, section headings and prose — but the catalog grid at `#catalog` (line 577) is empty markup filled by JS; 1 static brief link, 1 static playbook link; 572 KB document | Rich card: `og.png` 1200×630, og:title/description, `summary_large_image` — correct | The product (141 briefs) is invisible without JS; near-zero static internal linking; heaviest page on the site |
| `/b/47` (`b/47.html`) | Complete static article: breadcrumb, `h1` "The Fixer", lede, phases, the full verbatim brief in a `<pre>`, related-brief and playbook links — all crawlable without JS; ~10 KB gzipped | Per-brief card: `og/47.png` 1200×630, unique og:title/description, canonical `https://goal-prompts.vercel.app/b/47` | Only polish: no JSON-LD (the visible breadcrumb has no BreadcrumbList), heading skip h2→h4 |
| `/examples/` (`examples/index.html`) | Static list of real sample reports with title + meta description — fine for indexing; it is in the sitemap | **Nothing** — the page has no `og:*` tags, no `twitter:card`, and no `<link rel="canonical">` (grep confirms zero matches) | A shared link to the site's best proof asset unfurls as a bare URL |

---

## 3 · Findings

### SEO-1 · Homepage catalog invisible to non-rendering crawlers
- **Lens:** 3 Indexable content / 2 Crawlability
- **Where:** `index.html` (grid built by JS from `const DATA` at line 765; static HTML outside scripts links to only `/b/46` and `/p/codereview-collab`); source: `template.html`
- **Impact:** High — homepage anchor text and link equity to 176 detail pages is zero in static HTML; LLM crawlers and non-Google bots can't discover or summarize the catalog from the landing page; Google defers it to the render queue
- **Fix sketch:** emit a static catalog into `template.html` at build time — a plain `<ul>`/card list of all 141 brief links (id · name · one-liner) grouped by family, which the existing JS then enhances or replaces on load; or a static `<noscript>` index; or a build-generated HTML sitemap page linked from the footer
- **Effort:** M

### SEO-2 · 572 KB homepage HTML (507 KB inline script)
- **Lens:** 8 Performance signals
- **Where:** `index.html` (572,093 bytes total; 506,961 bytes inside `<script>`, dominated by all 141 full brief bodies in `const DATA`; ~165 KB gzipped vs ~10 KB for a `b/` page)
- **Impact:** Medium-high — download + parse cost on mobile hurts INP/TBT proxies; the bodies are only needed at copy time
- **Fix sketch:** strip `body` from the inline DATA and fetch `raw/<id>.md` (already deployed, network-only in `sw.js`) or a slimmed `catalog.json` on demand at copy time; keep offline copies via the service worker cache. Pairs naturally with SEO-1: static cards replace inline JSON
- **Effort:** M

### SEO-3 · One generic OG image for all 35 playbooks, Studio, and Vitals
- **Lens:** 6 Social unfurls
- **Where:** `build.py:1106` passes `{BASE}/og.png` for every playbook page; confirmed in `p/day1.html`, `studio.html`, `vitals.html` (all `og:image = /og.png`)
- **Impact:** Medium — playbooks are the shareable merchandising unit (storefront, badges, sponsors) yet every share renders the identical site-wide card, while briefs already get per-page `og/NN.png`
- **Fix sketch:** extend `scripts/og.py` to render per-playbook cards (name, brief count, family color bars from `build.FAMILY_COLORS`) into `og/p-<key>.png` and wire them through `playbook_detail`
- **Effort:** M

### SEO-4 · `/examples/` has no Open Graph tags and no canonical
- **Lens:** 6 Social unfurls / 4 Canonical
- **Where:** `examples/index.html` head (has title + description only; zero `og:`, `twitter:`, or `canonical` matches) — it is a sitemap URL (`sitemap.xml` line 6)
- **Impact:** Medium — the "see a real report" proof page, linked from every detail-page footer, unfurls as a bare link when shared
- **Fix sketch:** add the same og/twitter/canonical block the generated pages get (hand-add, or generate the page from `build.py`'s `page()` helper)
- **Effort:** S

### SEO-5 · `studio.html` and `vitals.html` lack `rel="canonical"`
- **Lens:** 4 Canonical & duplicates
- **Where:** `studio.html`, `vitals.html` heads (both have full og/meta but no canonical; the generated pages all have one via `build.py:774`)
- **Impact:** Low — `cleanUrls: true` in `vercel.json` 308-redirects `.html` variants, so real duplication risk is small; consistency and self-canonicalization are still worth having
- **Fix sketch:** add `<link rel="canonical" href="https://goal-prompts.vercel.app/studio">` (and `/vitals`) to the two source pages
- **Effort:** S

### SEO-6 · No structured data on the 141 brief pages or 35 playbook pages
- **Lens:** 5 Structured data
- **Where:** JSON-LD exists only on the homepage (`index.html:20`, SoftwareApplication); `b/*.html` and `p/*.html` have none (grep: 0 matches) despite visible breadcrumbs on every `b/` page
- **Impact:** Medium — 176 pages forgo BreadcrumbList (breadcrumb display in results) and content-type schema (Article/HowTo for briefs, ItemList for playbooks) that this content maps to cleanly
- **Fix sketch:** emit BreadcrumbList + a content schema per page from `brief_detail`/`playbook_detail` in `build.py` — all the data (title, description, dates, family) is already in hand at build time
- **Effort:** M

### SEO-7 · Sitemap has no `<lastmod>`
- **Lens:** 2 Crawlability
- **Where:** `sitemap.xml` (180 `<url>` entries, `<loc>` only), generated at `build.py:1389–1397`
- **Impact:** Low-medium — no freshness signal; crawlers recrawl all 180 URLs blindly and may be slow to pick up changed briefs
- **Fix sketch:** emit `<lastmod>` per URL from each source file's git commit date (or build date as a floor) in the sitemap writer
- **Effort:** S

### SEO-8 · The entire repo is deployed and indexable — duplicate text surfaces
- **Lens:** 2 Crawlability / 4 Duplicates
- **Where:** `vercel.json` (`outputDirectory: "."`) serves everything: `raw/*.md` (verbatim duplicates of each `b/` page's brief text), `prompts/**/*.md` (a third copy), root reports (`BUGS.md`, `FUNNEL.md`, …, linked from `b/` pages as "see a real report"), `build.py`, `tests/`, `docs/`; no `X-Robots-Tag` anywhere
- **Impact:** Low-medium — text/plain duplicates dilute crawl budget and can outrank the styled page for exact-phrase queries; bot fetches of `/raw/` also pollute the fetch-count usage metric (`sw.js:26`, `docs/usage-metrics.md`)
- **Fix sketch:** add `vercel.json` header rules — `X-Robots-Tag: noindex` for `/raw/(.*)` and `/prompts/(.*)` (they stay fetchable for agents, which is their purpose), and consider the same for root `*.md`
- **Effort:** S

### SEO-9 · Unfurl metadata gaps in the shared head template
- **Lens:** 6 Social unfurls
- **Where:** `build.py:758–787` (`page()`), mirrored in `template.html`, `studio.html`, `vitals.html`: no `og:site_name`, no `og:image:width/height/alt`, no `twitter:title`/`twitter:description`
- **Impact:** Low — most scrapers fall back to og:title/description; explicit tags remove ambiguity and add image alt for accessibility-aware unfurlers
- **Fix sketch:** add the four tags once in `page()` and once in each hand-authored head
- **Effort:** S

### SEO-10 · Heading-level skip on detail pages
- **Lens:** 7 Semantics & headings
- **Where:** `b/47.html` (and siblings): `h2` sections contain `h4` card/lens titles with no `h3` (phases do use `h3`, so the outline is inconsistent rather than flat)
- **Impact:** Low — outline still parses; one `h1` per page and descriptive link text are already in place everywhere checked
- **Fix sketch:** promote card/lens `h4`s to `h3` in `brief_detail`/`playbook_detail` (pure CSS restyle, no visual change)
- **Effort:** S

---

## 4 · Quick wins (shippable this week)

1. **SEO-4** — add og/twitter/canonical to `examples/index.html`; the site's proof page becomes shareable. (S)
2. **SEO-5** — add canonicals to `studio.html` and `vitals.html`. (S)
3. **SEO-7** — emit `<lastmod>` in the sitemap writer (`build.py:1393`). (S)
4. **SEO-8** — `X-Robots-Tag: noindex` headers for `/raw/` and `/prompts/` in `vercel.json`; also cleans the usage metric. (S)
5. **SEO-9** — `og:site_name` + image dimensions + twitter title/description in `page()`. (S)
6. **Start SEO-1** — even a minimal static footer/index of all 141 `/b/` links in `template.html` closes most of the discovery gap this week; the full static-card render can follow. (M, biggest reach)

Reach-blockers: none shipped today — nothing is unindexable and no `noindex` exists.
SEO-1 is the closest thing: not a blocker for Google, but a real reach ceiling for
every crawler that doesn't execute JS. Everything else is polish ranked by the traffic
it touches.

---

**Which of these fixes should I make?** SEO-1 (static homepage catalog) is the highest-reach; the five quick wins (SEO-4, 5, 7, 8, 9) are a small, safe batch — or name any subset and I'll start there.
