Skip to content

SEO audit: fix zero-CTR pages, title lengths, meta descriptions - #35

Open
michaelcolenso wants to merge 284 commits into
TimeMagazineLabs:masterfrom
michaelcolenso:seo-audit-fixes-2026-06-26
Open

SEO audit: fix zero-CTR pages, title lengths, meta descriptions#35
michaelcolenso wants to merge 284 commits into
TimeMagazineLabs:masterfrom
michaelcolenso:seo-audit-fixes-2026-06-26

Conversation

@michaelcolenso

Copy link
Copy Markdown

Changes from the NobodyNamed SEO audit

🔴 Critical — /about page (pos 3.4, 0% CTR)

  • Title: 48 → 71 chars — was wasting half the available SERP real estate
  • Meta desc: 79 → 158 chars — was too short to show in SERP, no CTA, now hooks with discovery intent

🔴 Critical — Name pages (featured snippet cannibalization)

  • Title template: shortened from ~75 to ~57 chars (was truncating on mobile)
  • Meta desc: no longer starts with the living-estimate number — that gave Google the answer directly in the snippet, killing clicks at pos add missing npmlog dependency to package.json #1

🟡 Static hub pages (pos 1.5, 0% CTR)

  • /comeback: title 33 → 58 chars
  • /extinct: title 33 → 55 chars
  • /endangered: title 32 → 57 chars
  • /rising: title 33 → 54 chars

🟡 Year pages

  • Meta desc: 211 → ~128 chars — was being truncated in SERP

Verification

  • TypeScript typecheck ✅ passes
  • All pages still SSR correctly
  • OG/twitter tags updated in lockstep

michaelcolenso and others added 30 commits May 5, 2026 04:14
…ns-in-implementation.md-5772s0

Rebrand site from Name Vitals to nobodynamed and update UI/branding
…-experience

Redesign NobodyNamed as an editorial cultural archive
…Content-Type headers

- era/[year]: replace innerHTML with textContent/replaceChildren to eliminate
  DOM-based XSS from API error strings
- classify.ts: guard rising status with latestCount > 0 — 2,059 names were
  classified "rising" despite zero births in the latest year (last5 avg can
  be positive from 2020–2023 data even when 2024 count is 0)
- compute.ts: query actual COUNT(*) from names after staging swap so
  meta.total_names reflects the real DB state rather than the in-memory page
  counter (was 8 off: 116,550 meta vs 116,558 actual)
- search.ts: add Content-Type: application/json; charset=utf-8 to the
  short-query early-return path for consistency with the success path
- twin/[name].ts: add charset=utf-8 to 404 Content-Type header

https://claude.ai/code/session_01UqHuPKevrgvffxke8h2hS4
- Resolve merge conflict markers in render-name.ts, og/[name].ts, name/[name]/index.ts
- Keep upstream redesign: dossier layout, generationForYear, describeStatus, NobodyNamed branding
- Add peerNames + yearTotals parallel fetch to name SSR function
- Restore statusLabel fix in renderFullPage title
- Add 404.html, d1-queries additions, sparkline opts, README/package.json updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both were set to "nobodynamed" which doesn't exist. The actual
Cloudflare resources are named "name-vitals" (confirmed via API):
- Pages project: name-vitals
- D1 database: name-vitals (UUID fc4741db-1f6d-457c-b4e4-675a4ea3ebc2)

https://claude.ai/code/session_01P9oRrKxc35ZGQN8nvtjNev
Dashboard confirms the Pages project is named nobodynamed, not
name-vitals. Keep only the D1 database_name fix (name-vitals is the
correct database name as shown in the dashboard binding).

https://claude.ai/code/session_01P9oRrKxc35ZGQN8nvtjNev
- [slug].ts: bail out on dot-filenames so static assets (.html, .svg)
  are no longer shadowed by the catch-all; add comeback/year/about redirects
- render-name.ts: affiliate link omitted unless AMAZON_ASSOCIATES_TAG is set;
  add displayStatus() so high-volume "endangered" names show "Past peak";
  fix og:image meta (svg+xml type + alt instead of wrong 1200x630 px dims);
  add twitter:image:alt
- name/[name]/index.ts: plumb AMAZON_ASSOCIATES_TAG from Pages env to renderer
- app.js: remove client-side empty affiliate link
- ingest-worker/wrangler.toml: add cron triggers (daily in May, weekly otherwise)
- og/[name].ts: document SVG platform limitations
- MIGRATION_TODO.md: document orphaned decade pages before next redeploy

https://claude.ai/code/session_01P9oRrKxc35ZGQN8nvtjNev
claude and others added 30 commits June 9, 2026 10:33
Editorial slate (content/blog/IDEAS.md) of data-driven blog concepts grounded
in the dataset's unique capabilities — living-population/median-age estimates,
pop-culture catalysts, geographic diffusion, gender migration, and wave
topology — organized into pillars, recurring franchises, and interactive plays.

Flagship post drafted in the house voice: the 'Karen' meme attached to a name
that peaked in 1965 and whose median living bearer is now ~60.

https://claude.ai/code/session_017RvtciWN8347rzqueRHiH8
Add docs/seo/2026-06-09-gsc-blog-demand.md analyzing 3 months of Search
Console data (581 queries). Key findings folded into the backlog:

- Unique-data intents are the moat: 'how many people are named X' (25) and
  'how old is X' (12) — answerable only via total_living_est / median_age.
- Decade/generation is the largest cluster (136 queries); those pages already
  rank page-1 but earn ~0% CTR (e.g. /names/1930s/ #4, 237 impr, 0 clicks).

Re-sequenced the backlog and added a Decades & Generations pillar.
Concrete SEO changes driven by docs/seo/2026-06-09-gsc-blog-demand.md:

- Name pages: emit FAQPage structured data from the existing visible Quick
  Answers (living population, median age, rarity, trend, geography), making the
  unique-data answers machine-readable for search + AI overviews. Questions
  mirror the on-page <h3>s exactly. Name metaTitle is unchanged.

- Year and decade pages: replace the generic '<decade> Baby Names' /
  'Top baby names in <year>' titles — which ranked page-1 at ~0% CTR — with the
  era's actual #1 boy and girl names plus a curiosity hook, and richer meta
  descriptions. Falls back to a generic title when leaders are unavailable.

Typecheck adds no new errors (pre-existing noUncheckedIndexedAccess errors in
unrelated api/* files remain); all 69 narrative tests pass.
Guard array/string index access in five viz API handlers so the repo
typechecks clean again (npm run typecheck now exits 0). All are
type-safety guards on pre-sized arrays — no behavior change:

- gender-crossings: m[i]/f[i]/years[i] fallbacks in the crossing scan
- name-universe: status.charAt(0) instead of status[0]
- peak-speed: median() handles possibly-undefined sorted entries
- suffix-waves / terminal-letters: resolve the target row before assigning
Decouple the SEO <title>/meta from the on-page <h1>: add optional
seoTitle/seoDescription per hub that lead with the page's signature
names plus a hook (e.g. 'Millennial Baby Names: Michael, Jessica & the
'80s-'90s Class') instead of a bare '<X> Baby Names'. The short title
still drives the <h1> and breadcrumb. Same CTR rationale as the year and
decade pages from the GSC demand analysis.
Replace directional life-table estimates with confirmed values from
name_enrichment_profiles and name_years:

- Living estimate: ~775,000 (was 'around a million')
- Median age: 65, core range 58-72 (was 'about 60'); born ~1961
- Peak: 1957 by births (40,591) vs 1965 by rank (#3) — now distinguished
- 2019 rank 662, 2020 rank 821 (were 660 / 831)
- Latest: 175 births in 2025; post-meme decline 440->332->229
- Drop the directional-estimate caveat; kept status: draft

Stays draft so it does not publish on merge.
Blog backlog, flagship draft & SEO changes from GSC demand analysis
- Create shared /assets/viz-theme.css with warm palette scoped to body.viz-page
- Convert all 25 viz pages from dark-space aesthetic to warm editorial theme:
  - Replace Space Grotesk/Mono with Avenir Next / IBM Plex Mono
  - Replace dark backgrounds (#080808, #0a0a0f) with --paper (#f4f0e7)
  - Replace neon accents (#818cf8, #f472b6, #34d399) with brand colors
  - Replace white-alpha grid lines with ink-alpha equivalents
  - Standardize all inline-styled headers/footers to site markup
- Remove viz/universe.html and viz/bloodlines.html
- Site-wide a11y & perf fixes:
  - Add aria-pressed to sex filter buttons
  - Add listbox pattern to search autocomplete
  - Add 180ms debounce to search input
  - Add loading=lazy to images, defer to scripts
  - Fix placeholder contrast (#6b6559)
  - Remove misleading dark theme-color meta tags
  - Add SRI hashes to D3 CDN scripts
  - Add editorial-nav styles
Unify viz pages with main site warm theme
A handoff doc capturing the six recurring query patterns (share-of-total,
rank-window, trajectory-around-a-year, cohort/age math, group-by-morphology,
location quotient) plus the data traps (reporting floor, 1989 truncation
artifacts, abbreviation noise, recency, D1 limits). Includes a schema
cheat-sheet and real worked examples for calibration.
Three-act data essay tying together conformity collapse, sound
convergence, and spelling fragmentation:
- content/blog/how-america-stopped-sharing-names.md (status: draft)
- apps/web/public/assets/blog-sharing-names.js — shared D3 module that
  renders four charts into div placeholders. Data + styling live in the
  asset (external script) so the blog pipeline's request-time linkify
  can't corrupt it; prose still auto-links names as usual.
- migration to publish into blog_posts (draft, won't go live on apply).

Charts: top-10 share by sex, top ending-letter share, the 3-family
spelling lifecycle, and the 2024 -eigh roster wall. Figures from prod D1.
- Plain <footer> tags were missing the .site class needed for style.css styling
- Rename inline content footer in suffix-waves.html to <p class="data-note">
…eature

Relocates HANDOFF.md, IMPLEMENTATION.md, MIGRATION_TODO.md, and ideas.md into
docs/ alongside other project documentation. Removes the abandoned phonetic
matching scaffolding (migration, shared lib, backfill script, tests) which
had no remaining references in active source.
Prevents accidentally committing service-account JSON keys (e.g.
.gsc-service-account.json) again.
…cific top-name titles, cultural context content

- Year pages: dynamic title with top names, 155-char meta desc, historical context paragraph per era
- Decade pages: title with top names, shorter meta desc, extended content above fold
…cific top-name titles, cultural context content

- Year pages: dynamic title with top names, 155-char meta desc, historical context paragraph per era
- Decade pages: title with top names, shorter meta desc, extended content above fold
Improve SEO for year and decade pages: shorter meta descriptions, spe…
Standalone, deploy-ready version of the video-game-names post:
- content/blog/press-start-to-name.md (status: published)
- apps/web/public/assets/blog-press-start.js (the roster widget)
- migration upserting the post into blog_posts as published

On merge, Cloudflare Pages ships the asset; the D1 post row is then
applied separately (blog:apply:remote, or via the Cloudflare MCP).
- apps/web/public/assets/press-start-hero.png (1200x630 OG/hero card)
- post: og_image -> the hero; hero <figure> at top of body
- regenerated migration to match (published)
…red snippet cannibalization

Changes from thorough GSC audit:

Static pages:
- /about: title 48→71 chars, meta desc 79→158 chars (was empty/flat, no CTA)
- /comeback: title 33→58 chars (was wasting 27 chars)
- /extinct: title 33→55 chars
- /endangered: title 32→57 chars
- /rising: title 33→54 chars

Dynamic name pages (packages/shared/src/generate-narrative.ts):
- Title template shortened from ~75 to ~57 chars (was truncating in SERP)
- Meta desc restructured: no longer starts with living-estimate number which triggered featured snippet cannibalization at #1 positions

Year pages (packages/shared/src/render-year.ts):
- Meta desc shortened from 211 to ~128 chars (was being truncated in SERP)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants