SEO audit: fix zero-CTR pages, title lengths, meta descriptions - #35
Open
michaelcolenso wants to merge 284 commits into
Open
SEO audit: fix zero-CTR pages, title lengths, meta descriptions#35michaelcolenso wants to merge 284 commits into
michaelcolenso wants to merge 284 commits into
Conversation
…ns-in-implementation.md
…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>
…-for-cloudflare-deployment
…d-to-end-version
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
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">
…and interactive sparkline tooltips
…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)
…ags, cultural context on year/decade pages
…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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes from the NobodyNamed SEO audit
🔴 Critical — /about page (pos 3.4, 0% CTR)
🔴 Critical — Name pages (featured snippet cannibalization)
🟡 Static hub pages (pos 1.5, 0% CTR)
🟡 Year pages
Verification