This reference covers the theme’s SEO metadata contracts, structured data output, sitemap and feed generation, and analytics/privacy compliance model.
Who this is for
- Implementors configuring SEO metadata, structured data, or analytics providers
- Developers extending the SEO component or adding new metadata outputs
- Editors verifying that content pages meet metadata requirements
Metadata Requirements (REQ-00099)
Every page must include the following metadata:
| Field | Required | Notes |
|---|---|---|
<title> | Yes | Unique per page |
<meta name="description"> | Yes | 50–160 characters |
| Canonical URL | Yes | Self-referential <link rel="canonical"> |
og:title | Yes | Can match <title> |
og:description | Yes | Can match description |
og:url | Yes | Canonical URL |
og:image | Yes | 1200×630 recommended |
The SEO component in src/core/components/base/SEO.astro implements this contract. It is included automatically in BaseLayout — do not add it manually in page templates.
Pages must not ship without a populated title and description. The validation pipeline enforces this at build time via lint:content-ingestion, which checks frontmatter for required fields.
Robots: All pages are indexable by default. Set noindex: true in frontmatter to exclude a page from search engines (e.g., QA-only showcase pages). Noindex is enforced via the HTML <meta name="robots" content="noindex,nofollow"> tag.
Site-level defaults (site name, default OG image, author) are configured in src/site/config/site.ts.
Structured Data — JSON-LD (REQ-00193)
The StructuredData component in src/core/components/base/StructuredData.astro emits JSON-LD <script> blocks in the <head>. It is included automatically in BaseLayout.
Supported schema types:
| Schema | Source | Condition |
|---|---|---|
| Organization | siteConfig.organization | Always emitted when configured |
| LocalBusiness | siteConfig.localBusiness | Emitted instead of Organization when configured (mutually exclusive) |
| WebPage | Page metadata | Emitted on indexable pages only |
| Article | Article frontmatter | Emitted on article pages (type="article") |
| BreadcrumbList | breadcrumbs layout prop | Emitted when breadcrumb data is passed to the layout |
The FAQSchema component (src/core/components/base/FAQSchema.astro) emits FAQPage JSON-LD inline in the body. Use it in any page or MDX content:
<FAQSchema items={[{ question: "What is this?", answer: "A test." }]} />
Open Graph (OG) Image Generation (REQ-00194)
Open Graph images (1200×630 PNG) are generated at runtime on first request for all indexable content entries and static pages. The route is src/pages/og/[...path].ts. (PLN-23)
Runtime behavior
Images are generated on-demand when first requested, then cached in memory for the container lifetime. Cache resets on restart or redeployment — no manual invalidation needed. This avoids rebuilding the full OG image set on every deploy.
- Content collection entries (pages, articles) are discovered via
getCollection(non-draft, non-noindex) - Static
.astropages opt in by exportingexport const meta = { title: "Page Title" }from their code fence - Dynamic routes, docs, and 404 pages are excluded
- First-request latency is typically under 0.5 seconds (Satori + resvg)
- Social crawlers receive
Content-Type: image/pngandCache-Control: public, max-age=31536000, immutable
Open Graph configuration (og.config.ts)
The OG template’s visual parameters are configurable via a core/site config pair:
- Core schema:
src/core/config/og.config.ts— exportsOgConfigtype andDEFAULT_OG_CONFIG - Site instance:
src/site/config/og.config.ts— imports defaults, spreads, overrides
| Field | Type | Default | Description |
|---|---|---|---|
siteName | string (optional) | siteConfig.siteName | Site name rendered in the OG image |
backgroundColor | string (hex) | "#1a1a2e" | Background color |
titleColor | string (hex) | "#e8e8e8" | Page title text color |
siteNameColor | string (hex) | "#888888" | Site name text color |
When siteName is omitted, the OG route resolves it from siteConfig.siteName at runtime. Provide it when the OG image should show a different name (e.g., shorter brand name).
A site with no og.config.ts customizations produces the same OG images as the original hardcoded template — zero setup cost.
Font asset management
The OG font is a site-owned asset, separate from the Astro Fonts API display fonts. Satori requires raw font bytes (TTF/OTF), not CSS @font-face declarations.
- Source TTF:
src/site/assets/og/Inter_24pt-Regular.ttf(SITE-OWNED) - Generated module:
src/site/assets/og/og-font-data.ts(SITE-OWNED, committed)
The font bytes are embedded as base64 in the generated TypeScript module, decoded once at module load via atob + Uint8Array.from. No filesystem access at runtime — compatible with Node, Cloudflare Workers, and Vercel Edge.
To replace the OG font:
- Replace
src/site/assets/og/Inter_24pt-Regular.ttfwith the new TTF - Run the regeneration command documented in the
og-font-data.tsfile header - Commit both the new TTF and the regenerated module
Font weight note: The template registers the same font buffer for weight 400 (site name) and weight 700 (page title). Satori synthesizes the bold weight. A variable-weight font or one with built-in bold produces better results than a single-weight file.
Per-page Open Graph image override
Override with ogImage in frontmatter to use a custom image instead of the generated one.
Open Graph image resolution chain
SEO.astro resolves OG images in this order:
- Explicit per-page
ogImage(frontmatter/props) — always wins - Generated
/og/{path}/— for eligible routes (non-noindex) siteConfig.ogImage— fallback for non-eligible routes (e.g., noindex pages)
For generated OG routes, SEO.astro also emits og:image:type, og:image:width, and og:image:height meta tags.
Sitemap (REQ-00093, REQ-00094)
Both the HTML sitemap (/sitemap/) and XML sitemap (/sitemap.xml) are prerendered page routes that use getCollection() directly. They share a unified filter: !draft && !noindex && !excludeFromSitemap, scoped to collections listed in siteConfig.sitemapCollections (default: ["pages", "articles"]).
Static .astro pages (e.g., /contact/, /search/) are discoverable via linkedRoute stubs — metadata-only .md files in src/content/pages/ whose linkedRoute field points to the actual URL. The [...slug].astro catch-all filters out linkedRoute entries to avoid route conflicts.
Pages are excluded from sitemaps when:
- Frontmatter has
draft: true - Frontmatter has
noindex: true(industry standard —noindexpages don’t belong in sitemaps) - Frontmatter has
excludeFromSitemap: true(escape hatch for indexed pages that shouldn’t appear) - The collection is not in
sitemapCollections
Robots.txt (REQ-00097)
Generated at /robots.txt from siteConfig.robots. Configure in src/site/config/site.ts:
robots: {
disallow: ["/private/"],
allow: ["/public/"],
additionalRules: "User-agent: Googlebot\nAllow: /",
},
RSS Feed (REQ-00183)
Generated at /rss.xml using @astrojs/rss. Includes published articles, excludes drafts and entries with excludeFromFeed: true in frontmatter. RSS autodiscovery is included in the HTML <head> on all pages.
Web Manifest (REQ-00098)
Served as a static file at public/site.webmanifest. Astro v6 introduced a conflict between trailingSlash: "always" and file-extension endpoints that made both SSR and prerendered routes unviable (D-C10-06-R1). Static files in public/ bypass Astro routing entirely.
Creating the Manifest
Edit public/site.webmanifest directly. Required fields:
| Field | Description | Example |
|---|---|---|
name | Full site name shown in install prompts | "Peak Performance Digital" |
short_name | Abbreviated name for home-screen icons | "PkPd" |
start_url | URL opened when the app launches | "/" |
display | Display mode | "standalone" |
background_color | Splash screen background (hex) | "#ffffff" |
theme_color | Browser chrome color (hex) | "#ffffff" |
icons | Array of icon objects (see below) | — |
Required Icon Files
Place these in public/:
| File | Size | Purpose |
|---|---|---|
android-chrome-192x192.png | 192×192 | Standard app icon |
android-chrome-512x512.png | 512×512 | High-res / splash |
Each icon entry needs src, sizes, and type:
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
Per-Site Setup
When creating a new site, update all values in public/site.webmanifest to match the site’s branding:
- Set
nameandshort_nameto the site’s name. - Set
background_colorandtheme_colorto match the site’s palette. - Generate icon PNGs at 192×192 and 512×512 and place them in
public/. - Verify the manifest loads at
/site.webmanifestafter build.
Trailing-Slash Enforcement (REQ-00145, REQ-00200)
Controlled by platformConfig.trailingSlash in src/site/config/platform.config.ts. When enabled, Astro config and middleware both use that value so non-trailing-slash URLs redirect to their canonical trailing-slash form.
Taxonomy Indexability (REQ-00193)
Configure via siteConfig.taxonomy:
taxonomy: {
categoryIndexable: true, // default
tagIndexable: false, // default
},
Use isTaxonomyIndexable("category" | "tag") from src/lib/get-taxonomy-indexability.ts in taxonomy routes to determine noindex state.
Analytics and Privacy (REQ-00100–REQ-00105)
Consent-Gated Analytics (CORE-OWNED)
The theme ships a CORE-OWNED consent mechanism that gates all analytics script loading behind user consent. This includes a ConsentBanner component (Accept / Reject / Required Only), consent state persistence in localStorage, and a consent-gated dynamic injection engine in Analytics.astro.
Key behaviors:
- When
consentRequiredistrue, no analytics scripts appear in the initial HTML — they are injected dynamically after the user accepts consent - Consent state is stored in
localStorageunder the keyanalytics-consentas a structured object with status and timestamp - The
ConsentResetcomponent provides consent revocation for placement on the privacy policy page - When
consentRequiredisfalse, scripts load immediately with no banner (pre-consent behavior for sites that don’t need consent gating)
For full configuration reference, consent states, script injection lifecycle, event taxonomy, and custom snippet setup, see Analytics & Consent.