ADR-029: SEO and Metadata Architecture
Status
Section titled “Status”Accepted (amended 2026-08-02: Props interface and title composition updated to match the
shipped Head.astro — see the annotated sections; the Head-molecule decision itself is
unchanged)
Context
Section titled “Context”Every page needs consistent <head> metadata: <title>, <meta description>, Open Graph tags, Twitter cards, canonical URLs, and optionally JSON-LD structured data. The implementation approach has significant implications for:
- SEO correctness: Missing or duplicate canonical URLs, incorrect OG tags
- Maintainability: Metadata logic scattered across layouts vs centralised
- Type safety: Untyped prop passing vs validated interfaces
- Extensibility: How easily pages can override defaults
This ADR documents the chosen architecture and the reasoning behind each decision.
Decision Drivers
Section titled “Decision Drivers”- Single source of truth: All metadata logic in one place, not duplicated across layouts
- Type-safe props: TypeScript interface prevents missing required fields
- Sensible defaults: Pages should work with minimal props; only title and description required
- Canonical URL correctness: Canonical must reflect the deployed site URL, not localhost
- OG image strategy: Must work without per-page custom images
Architecture Decision: Dedicated Head Molecule
Section titled “Architecture Decision: Dedicated Head Molecule”Option 1: Inline metadata in BaseLayout.astro
Section titled “Option 1: Inline metadata in BaseLayout.astro”<head> <title>{title}</title> <meta name="description" content={description} /> <!-- ... all OG tags inline ... --></head>Pros: Simple, no extra file
Cons:
BaseLayout.astrobecomes large and hard to read- Cannot reuse Head logic in alternative layouts (e.g. a minimal layout for landing pages)
- Harder to test metadata in isolation
Option 2: Dedicated Head.astro molecule (chosen)
Section titled “Option 2: Dedicated Head.astro molecule (chosen)”<head> <Head title={title} description={description} {...rest} /> <ClientRouter /></head>Pros:
BaseLayout.astrostays clean and readableHead.astrocan be reused across multiple layout variants- Metadata logic is isolated and testable
- Props interface is explicit and type-safe
Cons:
- One extra file to understand
Decision
Section titled “Decision”Use a dedicated Head.astro molecule at src/components/molecules/Head.astro with the following responsibilities:
Props Interface
Section titled “Props Interface”interface Props { title: string; // Required — page title (without site name suffix) description: string; // Required — meta description (150-160 chars ideal) image?: string; // OG image URL — defaults to site default OG image canonicalUrl?: string; // Override canonical — defaults to Astro.url noindex?: boolean; // Set true for admin/utility pages type?: 'website' | 'article'; // OG type — defaults to 'website' publishDate?: Date; // For article type — sets article:published_time}Amendment (2026-08-02): the shipped interface has evolved:
typeis nowogType(same union),publishDatewas replaced by anogArticleobject ({ publishedTime, modifiedTime, author, tags }),canonicalUrlis typedURL(defaulting tonew URL(Astro.url.pathname, Astro.site)), and apreconnectDomains?: string[]prop was added.src/components/molecules/Head.astrois authoritative for the current shape.
Title Format
Section titled “Title Format”Page titles are formatted as {title} | {siteName} where siteName comes from siteMetadata.title in src/config.ts (amended 2026-08-02: originally attributed to astro.config.mjs, which only holds the site URL). Head.astro itself appends the suffix — skipping it when the page title already starts with the site name — and individual pages pass only their own title string (amended 2026-08-02: originally attributed the suffixing to BaseLayout, which merely forwards the title prop).
Why not include the site name in each page’s title prop? Prevents duplication when the site name changes, and keeps page-level titles clean.
Canonical URL Strategy
Section titled “Canonical URL Strategy”Canonical URLs are generated from Astro.url by default, which reflects the site value set in astro.config.mjs. This means:
- In development:
http://localhost:4321/blog/my-post/ - In production:
https://yourdomain.com/blog/my-post/
The site value in astro.config.mjs must be set to the production URL before deploying. The SITE_URL environment variable overrides this for CI/CD environments.
Why not hardcode canonical URLs? Hardcoded URLs break in staging environments and require manual updates when domains change.
OG Image Strategy
Section titled “OG Image Strategy”A default OG image (/og-default.png) is used when no page-specific image is provided. This ensures every page has a valid OG image for social sharing without requiring per-page image creation.
For blog posts and projects with a cover image (the schema field), the layout passes the optimised image URL as the image prop.
Structured Data (JSON-LD)
Section titled “Structured Data (JSON-LD)”(Amended 2026-08-13 to ship-truth:) Head.astro injects a JSON-LD @graph with WebSite + Organization on every page, adding BlogPosting (headline, dates, author, image) on article pages via ogType="article". Project pages get the site-level graph but no project-specific schema (no standard schema fits portfolio projects cleanly).
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- All metadata logic in one file — easy to audit and update
- Type-safe props catch missing fields at build time
- Canonical URLs are always correct relative to the deployed environment
- Default OG image ensures no page is missing social metadata
Negative
Section titled “Negative”- Users must understand the
Headmolecule to customise metadata - JSON-LD is minimal by default — users with rich structured data needs must extend it
Neutral
Section titled “Neutral”- Twitter/X card type is
summary_large_imageby default — appropriate for most content robotsmeta tag defaults toindex, follow; setnoindex: truefor utility pages
Validation
Section titled “Validation”- No missing OG tags: Every page must have
og:title,og:description,og:image,og:url - Canonical correctness: Canonical URL must match the page URL in production
- No duplicate titles:
<title>must be unique per page - Lighthouse SEO: Must score 95+ with no meta description warnings
References
Section titled “References”- Open Graph Protocol
- Twitter Card Documentation
- JSON-LD Structured Data (Google)
- ADR-010: Social Share URL Utility
- ADR-020: Page Performance Patterns
Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1:
<title>and OG meta tags are emitted only byHead.astro— no other layout or page declares them. - TC-2: generated OG images are current with their inputs.
- TC-1:
- Checks:
- TC-1 → check
head-single-source(status: warn) - TC-2 →
og:checkinquality:ci(status: block, pre-existing gate)
- TC-1 → check
- Not machine-checkable: metadata quality (title/description wording, default appropriateness).
- Graduation log: (empty at creation; entries added when a check changes status)
Date: 2026-02-18
Participants: Template maintainers
Outcome: Accepted