Skip to content

feat: SEO hardening — P0 meta fixes, auto JSON-LD, headers, RSS, recipes docs - #9

Merged
vairi merged 5 commits into
mainfrom
feat/seo-hardening
Apr 17, 2026
Merged

feat: SEO hardening — P0 meta fixes, auto JSON-LD, headers, RSS, recipes docs#9
vairi merged 5 commits into
mainfrom
feat/seo-hardening

Conversation

@vairi

@vairi vairi commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Executes the P0 + P1 items from the SEO audit against SEOHead.astro, the four sites, and Meridian's new insights collection. P2 items are documented in docs/seo-recipes.md rather than baked in.

What changed

packages/shared-ui/src/components/SEOHead.astro

  • Emits full robots directives (index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1) on indexable pages; noindex, nofollow on opted-out pages
  • Suppresses canonical when noindex=true — two contradictory signals hurt rankings
  • og:image now includes width, height, and alt; only emits when an image is actually supplied (no more broken fallback to a non-existent /images/og-default.png)
  • twitter:card defaults to summary_large_image when an og:image is present
  • Drops meta keywords emission — kept the prop accepted with @deprecated so page call sites don't break
  • Declares <link rel="icon" href="/favicon.svg"> — every site ships one but nothing referenced it
  • Auto-generates a linked WebSite + Organization JSON-LD @graph from props: siteName, Astro.site, logoSrc, and socialLinks (derived as sameAs). User-supplied structuredData merges into the same graph so @id refs resolve. Opt out via skipAutoJsonLd.
  • New pass-through props on BaseLayout: ogImageAlt, ogType, twitterHandle, noindex, rssUrl, skipAutoJsonLd

sites/*/public/_headers

  • 1-year immutable cache on /_astro/* (covers hashed JS, CSS, and fonts under /_astro/fonts/)
  • No-Vary-Search on / to strip UTM + ad-click params from the CDN cache key

sites/meridian-advisory.com

  • @astrojs/rss + /rss.xml endpoint generated from the insights collection
  • Insights pages pass rssUrl="/rss.xml" so <link rel="alternate" type="application/rss+xml"> appears in head
  • Insight detail pages emit og:type=article

docs/seo-recipes.md (new)

Documents the P2 items — per-page OG images (Satori + Sharp), git-based sitemap lastmod, llms.txt, markdown alternates, IndexNow, FuzzyRedirect, view transitions, schema endpoints, NLWeb, build-time validation. Linked from README and CLAUDE.md.

Why these specifically

All baked-in items ride on code the starter already has (a shared SEOHead component, socialLinks already in every site-config) and pay off on every site without user action. The P2 items need either extra deps (satori/sharp), per-site content structure decisions (which collections to emit as llms.txt), or an opinionated UX choice (view transitions) — so they belong in docs, not defaults.

Test plan

  • bun run build --force — all 4 sites build clean from scratch
  • Meridian home emits a valid linked JSON-LD @graph (WebSite + Organization with @id refs and sameAs from social links)
  • Insights pages emit full robots directives, correct canonical, and <link rel="alternate" type="application/rss+xml">
  • /rss.xml produces valid RSS with 3 items, correct pubDates, author, categories
  • _headers copied into each site's dist/ at build
  • Validate one insight page in Google Rich Results Test after deploy
  • Verify Cloudflare Pages honours the /_astro/* immutable rule (network tab after deploy)

Follow-ups (not in this PR)

- Emit full robots directives on indexable pages (max-snippet:-1,
  max-image-preview:large, max-video-preview:-1) so Google can show
  rich snippets.
- Suppress canonical when noindex=true — two contradictory signals hurt
  rankings.
- og:image now includes width/height/alt when provided; only emits if
  an ogImage is actually supplied (no more broken fallback to a
  non-existent /images/og-default.png).
- twitter:card defaults to summary_large_image when an og:image is
  present (previously always summary unless user opted in).
- Drop meta keywords emission — ignored by all major search engines.
  Prop retained with @deprecated JSDoc so existing page call sites
  don't break.
- Declare <link rel="icon"> pointing at /favicon.svg — every site
  ships one but no head tag referenced it.
- Add optional rssUrl/rssTitle props to advertise RSS feeds via
  <link rel="alternate" type="application/rss+xml"> (wired up in
  the Meridian RSS commit).
BaseLayout now derives siteUrl, logoUrl, and sameAs (from socialLinks)
and passes them to SEOHead. SEOHead builds a linked @graph with:

- WebSite node pointing at the site root with an inLanguage from locale
- Organization node with logo (ImageObject) and sameAs (social URLs)
- @id references so the two nodes are linked and additional nodes added
  via the structuredData prop can reference them

User-supplied structuredData is merged into the same @graph (its
@context is stripped to avoid duplication). Pages that need a fully
custom graph can pass skipAutoJsonLd to opt out.

Also exposes ogImageAlt, ogType, twitterHandle, noindex, rssUrl on
BaseLayout so pages can wire them without importing SEOHead directly.
…ry-Search

Ships the same _headers template across starter + all three demo sites.
Cloudflare Pages and Netlify both honour this format; other CDNs ignore
unknown directives so it's safe.

- /_astro/* (including /_astro/fonts/*) gets a 1-year immutable cache —
  filenames are content-hashed so bumping the cache key isn't needed.
- / gets No-Vary-Search to strip UTM + ad-click tracking params from the
  CDN cache key so marketing attribution doesn't fragment the cache.
Adds @astrojs/rss and /rss.xml endpoint generated from the insights
content collection. Insights list and detail pages advertise the feed
via <link rel="alternate" type="application/rss+xml"> (wired through
BaseLayout → SEOHead rssUrl prop).

Detail pages also now emit og:type=article so social unfurls and
crawlers correctly classify them.
@vairi
vairi merged commit d7f9179 into main Apr 17, 2026
1 check passed
@vairi
vairi deleted the feat/seo-hardening branch April 17, 2026 19:11
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