Skip to content

fix(seo): bot snapshots had no h1, no internal links, leaked canonicals - #29

Merged
gogrowth-co merged 1 commit into
mainfrom
fix/seo-snapshot-h1-links-canonical
Aug 23, 2026
Merged

fix(seo): bot snapshots had no h1, no internal links, leaked canonicals#29
gogrowth-co merged 1 commit into
mainfrom
fix/seo-snapshot-h1-links-canonical

Conversation

@gogrowth-co

Copy link
Copy Markdown
Owner

What Googlebot was actually reading

cloudflare-worker.js routes any bot UA sending Accept: text/html to the seo-snapshot edge function. Its output is the indexed page. Humans never see it, so this was invisible in the browser.

Two generators built that output independently, and they drifted:

path output
regenerate-snapshot (on publish) header, h1, author meta, footer, links
seo-snapshot self-heal fallback <body><article>{content}</article></body>

The fallback had no <h1>, no internal links, and only a minimal Article schema. It uploads what it generates into the seo-snapshots bucket, and the bucket is served first from then on — so every self-heal permanently replaced a good snapshot with a broken one.

Measured on the live site before this fix

111 sitemap URLs, parsed with a real browser engine, JS disabled:

  • 77 of 111 URLs served zero <h1>
  • median internal links per article: 0
  • GSC: rich_results null site-wide; defi-marketing-team-metrics regressed from indexed to Crawled – currently not indexed

The canonical leak

Several articles are authored in the CMS as complete standalone HTML documents. Embedded inside <article>, that leaked a second <title> and a second <link rel="canonical">. On the BR and ES copies of web2-vs-web3-marketing the leaked canonical pointed at /publications/web2-vs-web3-marketing-br — which GSC classifies as a Soft 404. The article was telling Google its authoritative version was a dead URL.

Changes

  • _shared/snapshot-html.ts is now the only builder of snapshot HTML, used by both functions, so the paths cannot drift again.
  • sanitizePublicationHtml strips nested DOCTYPE/html/head/body and any stray title/link/meta.
  • Snapshots emit an h1 (when content lacks one), a nav with crawl paths, hreflang siblings, a footer link, and BreadcrumbList alongside Article.
  • validateSnapshot() gates every upload: exactly one title, one canonical matching the expected URL, an h1, ≥3 internal links, no nested scaffolding, JSON-LD present, 150+ words. Failures are refused, not stored.
  • seo-snapshot validates on storage read too, so objects poisoned by the old code are rebuilt in place rather than served forever.

Verification

  • deno check passes on all three files.
  • Builder run against the real CMS content for all three locales of web2-vs-web3-marketing: 1 title, 1 correct canonical, h1 present, 4 internal links, validation PASS.
  • Negative control: the old lean template is correctly rejected by the guard.

Deploy

Edge functions here deploy only via Lovable Cloud (see DEPLOY.md). After merge, trigger the deploy, then regenerate snapshots with {all:true}.

🤖 Generated with Claude Code

…ed canonicals

Googlebot, Bingbot, GPTBot, ClaudeBot and PerplexityBot do not read the SPA.
cloudflare-worker.js routes any bot UA sending `Accept: text/html` to the
seo-snapshot edge function, so its output IS the indexed page.

Two generators were building that output independently:

  - regenerate-snapshot (publish path)  -> header, h1, author meta, footer
  - seo-snapshot's inline self-heal     -> <body><article>{content}</article>

The self-heal version had no <h1>, no internal links and only a minimal
Article schema. Because it UPLOADS what it generates into the seo-snapshots
bucket, and the bucket is served first from then on, every self-heal
permanently replaced a good snapshot with a broken one.

Measured on the live site before this fix (111 sitemap URLs, parsed with a
real browser engine, JS disabled):
  - 77 of 111 URLs served zero <h1>
  - median internal links per article: 0
  - GSC: rich_results null site-wide; defi-marketing-team-metrics regressed
    from indexed to "Crawled - currently not indexed"

Separately, several articles are authored in the CMS as complete standalone
HTML documents. Embedding that inside <article> leaked a second <title> and a
second <link rel="canonical">. On the BR and ES copies of
web2-vs-web3-marketing the leaked canonical pointed at
/publications/web2-vs-web3-marketing-br, which GSC classifies as a Soft 404:
the article was telling Google its authoritative version was a dead URL.

Changes:
  - _shared/snapshot-html.ts is now the only builder of snapshot HTML, used by
    both functions, so the two paths cannot drift again.
  - sanitizePublicationHtml strips nested DOCTYPE/html/head/body plus any
    stray title/link/meta, so full-document content can never leak head tags.
  - Snapshots now emit an h1 (when the content lacks one), a nav with crawl
    paths to the hub, about, tools and the audit page, hreflang siblings, a
    footer link, and BreadcrumbList alongside Article schema.
  - validateSnapshot() gates every upload in both functions: exactly one
    title, exactly one canonical matching the expected URL, an h1, at least
    three internal links, no nested document scaffolding, JSON-LD present,
    150+ words. A failing snapshot is refused, not stored.
  - seo-snapshot also validates on storage READ, so objects poisoned by the
    old code are detected and rebuilt in place instead of being served
    forever.

Verified: deno check passes on all three files; the builder was run against
the real CMS content for all three locales of web2-vs-web3-marketing
(1 title, 1 correct canonical, h1 present, 4 internal links, validation PASS)
and the old lean template is correctly rejected by the guard.

Co-Authored-By: Claude Fable 5 <[email protected]>
@netlify

netlify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploy Preview for mangabeira ready!

Name Link
🔨 Latest commit d8ff458
🔍 Latest deploy log https://app.netlify.com/projects/mangabeira/deploys/6a8b424f34218c00087e0cc9
😎 Deploy Preview https://deploy-preview-29--mangabeira.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@gogrowth-co
gogrowth-co merged commit 85f0b34 into main Aug 23, 2026
4 of 8 checks passed
@gogrowth-co
gogrowth-co deleted the fix/seo-snapshot-h1-links-canonical branch August 23, 2026 18:56
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: gogrowth-co/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d440c7c4-566f-4283-9c5d-f5ed8d91829c


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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