ADR-029: SEO and Metadata Architecture
Documents the decision to extract all <head> metadata into a dedicated Head molecule, the structured data strategy, canonical URL generation, and OG/Twitter card implementation.
Last updated
Status
Accepted
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
- 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
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)
<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
Use a dedicated Head.astro molecule at src/components/molecules/Head.astro with the following responsibilities:
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}Title Format
Page titles are formatted as {title} | {siteName} where siteName comes from astro.config.mjs site metadata. The BaseLayout appends the suffix — individual pages pass only their own title string.
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
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
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 heroImage, the layout passes the optimised image URL as the image prop.
Structured Data (JSON-LD)
JSON-LD is injected for two content types:
- Blog posts:
Articleschema withheadline,datePublished,dateModified,author,image - Site root:
WebSiteschema withname,url,description
JSON-LD is not injected on every page — only where it provides meaningful SEO value. Project pages do not use JSON-LD by default (no standard schema fits portfolio projects cleanly).
Consequences
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
- Users must understand the
Headmolecule to customise metadata - JSON-LD is minimal by default — users with rich structured data needs must extend it
Neutral
- Twitter/X card type is
summary_large_imageby default — appropriate for most content robotsmeta tag defaults toindex, follow; setnoindex: truefor utility pages
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
- Open Graph Protocol
- Twitter Card Documentation
- JSON-LD Structured Data (Google)
- ADR-010: Social Share URL Utility
- ADR-020: Page Performance Patterns
Date: 2026-02-18
Participants: Template maintainers
Outcome: Accepted