feat(seo): quick wins - dup meta + og:image 1200x630 + robots.txt + insights#33
Merged
Conversation
…nsights Three production bugs caught by curl-against-live-site investigation plus a brand og:image and a robots.txt for the API surface. 1. BlogArticlePage emitted TWO `<meta name="description">` tags per article (the shell's generic plus an appended article-specific one). Google may pick the wrong one. Affects every prerendered blog route (~624). Fix: mutate the existing meta tag via setAttribute instead of appendChild, symmetric with how src/hooks/usePageMeta.js handles top-level pages. The cleanup restores the previous content on unmount. 2. Same component emitted `<meta property="og:*">` tags WITHOUT the `property=` attribute because the previous code used `meta[attr] = key` and HTMLMetaElement has no JS property for `property`. The elements shipped as `<meta content="..." data-blog="1">`, invisible to crawlers. The shell's generic og:title and og:description were what Facebook/Twitter/Slack/LinkedIn cards actually showed. Fix: mutate the existing og:* tags (which the shell already ships with property= set correctly) via setAttribute. Twitter card tags handled the same way. 3. api.cercol.team had no robots.txt; Googlebot was probing and getting 404. New endpoint `GET /robots.txt` returning "User-agent: *\nDisallow: /\n" via PlainTextResponse. The API is not crawlable content; this stops crawl-budget waste. Brand og:image: - New `public/og-image.png` (1200x630, 24 KB) generated by `scripts/generate_og_image.py`. Uses Cercol brand palette (#0047ba background, #cf3339 accent, #f1c22f, #427c42), Playfair Display Bold for the wordmark, Roboto Regular for the tagline. Fonts pulled from fonts.gstatic.com (versioned URLs) on first run and cached under scripts/.og_fonts/ (gitignored). - index.html: og:image -> /og-image.png, twitter:card upgraded from `summary` to `summary_large_image`, og:image:width and og:image:height and og:image:type and og:image:alt added. Regression tests in api/tests/test_seo.py: - exactly-1 `<meta name="description">` per route. - exactly-1 `<link rel="canonical">` per route. Both alongside the existing H1 guard. Skip-friendly when dist/ has not been prerendered, just like the H1 test. api/tests/test_robots.py: new test asserts the endpoint returns 200 with the expected text/plain body. PageSpeed unblocked: with the IP restriction lifted on the GCP API key, an immediate re-run of jobs.pagespeed_ingest wrote 14 rows to cercol_seo.pagespeed_runs (7 URLs x 2 devices). docs/seo/insights-2026-05.md captures the first GSC-bulk-export snapshot: USA-Denmark-Norway-Netherlands traffic, zero impressions from CA/FR/DE markets, three pages in the "quick wins" 8-20 SERP position band, and the Bing-error spike correlated with the May 17 Caddy outage. ROADMAP: 17.6.q row added with the three fixes; 17.6.7 backlog expanded with editorial quick wins and per-route og:image variants. Gate results: pytest 117 + 21 skipped (+2), vitest 199, vite build OK, ruff 8 (baseline). og-image.png verified 1200x630 RGB via Pillow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Three production bugs caught by curl-against-live-site, plus a brand og:image and a robots.txt for the API.
Bugs fixed
<meta name="description">(shell + appended). Google could pick the generic one. Affects ~624 prerendered blog routes. Fix: BlogArticlePage now mutates the existing meta tag instead of appending a new one, symmetric withusePageMeta.<meta property="og:*">tags shipped WITHOUTproperty=attribute because the previous code usedmeta[attr] = keyand HTMLMetaElement has no JS property forproperty. Crawlers saw only the shell's generic OG tags. Fix: mutate the existing og:* tags via setAttribute.api.cercol.teamhad no robots.txt (Googlebot probing 404). New endpoint at the API.Adds
public/og-image.png1200x630, brand colours, Playfair Display + Roboto. Generated byscripts/generate_og_image.py(reproducible). Twitter card upgraded tosummary_large_image.docs/seo/insights-2026-05.mdfirst GSC bulk-export snapshot: geography (USA/DK/NO/NL), language coverage, quick wins (pos 8-20), Bing crawl error spike correlated with the May 17 Caddy outage.Tests + ops
api/tests/test_seo.py: exactly-1<meta name="description">and exactly-1<link rel="canonical">per route (alongside the existing H1 guard).api/tests/test_robots.py:/robots.txtreturns 200 withDisallow: /.🤖 Generated with Claude Code