5.9 KiB
Context
Step 001 produced a working Astro project with Tailwind v4 brand tokens, Archia font-face declarations, and a smoke-test index.astro that manually constructs its own <html> document. Every subsequent page (homepage, about, AI Launchpad, contact, privacy — plus /nl/* variants) will need the same HTML shell with correct <head> meta, hreflang, OG tags, and body defaults. Without a shared layout this is copied per page, creating drift and SEO inconsistency.
Current state of note:
global.cssis Tailwind v4 (@import "tailwindcss"+@theme) — notailwind.config.mjs- Archia
@font-facedeclarations already live inglobal.css public/hasfavicon.svgandfavicon.ico- No
src/layouts/,src/components/, orsrc/content/directories yet
Goals / Non-Goals
Goals:
- Single
BaseLayout.astrothat every page wraps its content in - Correct SEO
<head>: charset, viewport, favicon, font preloads, canonical, hreflang, OG, Twitter card - Body defaults applied once (Midnight bg, Snow text, Archia font)
- JSON-LD injection mechanism (both prop and named slot)
- Stub
Nav.astroandFooter.astroso BaseLayout can import them (filled in steps 003+) en.json/nl.jsoncontent files +i18n.tsloader for global meta fieldsindex.astroupdated to useBaseLayout
Non-Goals:
- Actual nav or footer UI (step 003)
- Page-specific JSON-LD schemas (each page step handles its own)
- OG image file creation (deferred —
ogImageprop will be empty string initially) - Any client-side JavaScript
@astrojs/imageor picture optimisation (images not needed in this step)
Decisions
1. hreflang: explicit props, not derived
Decision: BaseLayout accepts two explicit props — canonicalUrl: string and alternateUrl: string. The component does not compute the alternate URL.
Rationale: EN routes are /about, NL routes are /nl/about — a simple /nl/ prefix works for most paths, but /contact vs /nl/contact or future edge cases (slugs, catch-alls) could diverge. Explicit props make each page's intent clear and avoid regex surprises. Callers always know their own routes.
Alternative considered: Auto-derive by prepending/stripping /nl/ based on locale prop. Rejected: fragile for edge cases, magic at a distance.
2. Font preloads: critical weights only
Decision: Preload only Archia Regular (400), SemiBold (600), and Bold (700). Thin (100) and Light (300) load on demand.
Rationale: Each <link rel="preload"> is an unconditional network request. Regular is needed for all body copy; SemiBold and Bold are used for headings and CTAs — the elements visible above the fold. Thin and Light appear only in specific decorative contexts (if at all) and are not worth the preload cost.
3. JSON-LD: prop for common case + named slot for extras
Decision: BaseLayout accepts an optional jsonLd?: Record<string, unknown> prop. If provided, it is serialized with JSON.stringify and injected as <script type="application/ld+json" set:html={...}>. A <slot name="jsonld" /> is also provided for pages that need additional or multiple JSON-LD blocks.
Rationale: Most pages will pass one structured data object (e.g., ProfessionalService, WebPage). The prop keeps call sites clean. The named slot exists as an escape hatch for pages like the homepage that may need multiple JSON-LD entries.
4. Nav/Footer: imported stub components
Decision: BaseLayout.astro imports Nav.astro and Footer.astro directly as Astro components. Both are created as empty stubs (render nothing). Step 003 fills in Nav.astro; a later step fills Footer.astro.
Alternative considered: Named slots (<slot name="nav" />). Rejected: would require every page to manually import and wire up Nav, which is error-prone and defeats the purpose of a shared layout.
5. Body defaults: Tailwind classes on <body>
Decision: Apply class="bg-midnight text-snow font-archia" directly on the <body> element in BaseLayout. No @layer base rule added to global.css.
Rationale: These are layout-level defaults, not global resets. Keeping them as explicit Tailwind classes on the element makes them visible and overridable per-section without specificity surprises. Consistent with how index.astro already applies them.
6. Content JSON: flat meta key at top level
Decision: Both en.json and nl.json start with a top-level "meta" key containing siteName, defaultTitle, defaultDescription, and defaultOgImage. Subsequent steps add page-specific keys ("home", "about", etc.) at the same top level.
Rationale: Flat top-level keys by section keep the file scannable and allow each step to add its own section without touching others. The "meta" key is reserved for global/shared values.
7. i18n helper: synchronous import, not dynamic fetch
Decision: i18n.ts exports a function getContent(locale: string) that does a static import of each JSON and returns the correct one. No dynamic fetch or fs.readFile.
Rationale: Astro builds statically — all pages are rendered at build time. Static imports are tree-shakeable and type-safe. Dynamic fetch is unnecessary complexity.
Risks / Trade-offs
- OG image missing → Social card previews will have no image until a default OG image is created. Mitigation:
ogImageprop defaults to empty string; the<meta property="og:image">tag is only rendered whenogImageis truthy. - Stub Nav/Footer render nothing → The smoke-test page will show Midnight body with no navigation. This is expected and clearly temporary. Mitigation: none needed — step 003 immediately follows.
- hreflang on smoke-test page →
index.astro(the brand smoke-test) will need placeholdercanonicalUrlandalternateUrlprops. Use"/"and"/nl"as stand-ins. This is fine since the smoke-test page will be replaced when the real homepage is built.