Skip to content

feat(seo): quick wins - dup meta + og:image 1200x630 + robots.txt + insights#33

Merged
miquelmatoses merged 1 commit into
mainfrom
feat/seo-quick-wins
May 23, 2026
Merged

feat(seo): quick wins - dup meta + og:image 1200x630 + robots.txt + insights#33
miquelmatoses merged 1 commit into
mainfrom
feat/seo-quick-wins

Conversation

@miquelmatoses
Copy link
Copy Markdown
Collaborator

Three production bugs caught by curl-against-live-site, plus a brand og:image and a robots.txt for the API.

Bugs fixed

  1. Blog articles shipped TWO <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 with usePageMeta.
  2. <meta property="og:*"> tags shipped WITHOUT property= attribute because the previous code used meta[attr] = key and HTMLMetaElement has no JS property for property. Crawlers saw only the shell's generic OG tags. Fix: mutate the existing og:* tags via setAttribute.
  3. api.cercol.team had no robots.txt (Googlebot probing 404). New endpoint at the API.

Adds

  • public/og-image.png 1200x630, brand colours, Playfair Display + Roboto. Generated by scripts/generate_og_image.py (reproducible). Twitter card upgraded to summary_large_image.
  • docs/seo/insights-2026-05.md first 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

  • New regression guards in 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.txt returns 200 with Disallow: /.
  • PageSpeed key IP restriction lifted by Miquel; re-ran ingest and pagespeed_runs now has 14 rows (7 URLs × 2 devices).
  • pytest 117 + 21 skipped (+2), vitest 199, vite build OK, ruff 8 baseline.

🤖 Generated with Claude Code

…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>
@miquelmatoses miquelmatoses merged commit 11767c8 into main May 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant