step 002 complete
This commit is contained in:
80
openspec/specs/base-layout/spec.md
Normal file
80
openspec/specs/base-layout/spec.md
Normal file
@@ -0,0 +1,80 @@
|
||||
## Purpose
|
||||
|
||||
Defines the `BaseLayout.astro` component that provides the complete HTML document shell for every page of the Qumo website, including all `<head>` meta tags, font preloading, hreflang alternates, JSON-LD support, and the Nav/Footer stub components.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: BaseLayout provides complete HTML document shell
|
||||
`src/layouts/BaseLayout.astro` SHALL render a complete HTML document (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) that every page can wrap its content in. It SHALL accept the following props:
|
||||
- `title: string` — page-specific `<title>` and OG title
|
||||
- `description: string` — page-specific meta description and OG description
|
||||
- `locale?: string` — BCP 47 language tag, default `"en"`
|
||||
- `canonicalUrl: string` — absolute canonical URL for this page
|
||||
- `alternateUrl: string` — absolute URL of the alternate-locale version of this page
|
||||
- `ogImage?: string` — absolute URL for `og:image`; tag is omitted when falsy
|
||||
- `jsonLd?: Record<string, unknown>` — structured data object to serialize as JSON-LD
|
||||
|
||||
#### Scenario: Page renders with all head meta tags
|
||||
- **WHEN** a page wraps its content in `<BaseLayout>` with all props provided
|
||||
- **THEN** the rendered HTML `<head>` SHALL contain: `<meta charset="utf-8">`, `<meta name="viewport">`, `<link rel="icon">` for both SVG and ICO favicons, `<link rel="canonical">`, `<link rel="alternate" hreflang>` for both locales, `<meta property="og:*">` tags, `<meta name="twitter:*">` tags, and `<title>`
|
||||
|
||||
#### Scenario: OG image tag omitted when ogImage is falsy
|
||||
- **WHEN** `ogImage` prop is an empty string or not provided
|
||||
- **THEN** no `<meta property="og:image">` tag SHALL appear in the rendered HTML
|
||||
|
||||
#### Scenario: Body has brand defaults
|
||||
- **WHEN** any page uses BaseLayout
|
||||
- **THEN** the `<body>` element SHALL have Tailwind classes `bg-midnight text-snow font-archia` applied
|
||||
|
||||
### Requirement: BaseLayout preloads critical Archia font weights
|
||||
The `<head>` SHALL contain `<link rel="preload" as="font" type="font/woff2" crossorigin>` for Archia Regular (400), SemiBold (600), and Bold (700) woff2 files. Thin (100) and Light (300) SHALL NOT be preloaded.
|
||||
|
||||
#### Scenario: Correct preload tags rendered
|
||||
- **WHEN** the page HTML is rendered
|
||||
- **THEN** exactly three font preload links SHALL appear in `<head>`: one each for `archia-regular-webfont.woff2`, `archia-semibold-webfont.woff2`, and `archia-bold-webfont.woff2`
|
||||
|
||||
### Requirement: BaseLayout renders hreflang alternates
|
||||
The `<head>` SHALL contain `<link rel="alternate" hreflang>` tags for both `en` and `nl` locales, plus an `x-default` pointing to the EN URL.
|
||||
|
||||
#### Scenario: EN page hreflang output
|
||||
- **WHEN** a page passes `locale="en"`, `canonicalUrl="https://qumo.io/about"`, `alternateUrl="https://qumo.io/nl/about"`
|
||||
- **THEN** the head SHALL contain:
|
||||
- `<link rel="alternate" hreflang="en" href="https://qumo.io/about">`
|
||||
- `<link rel="alternate" hreflang="nl" href="https://qumo.io/nl/about">`
|
||||
- `<link rel="alternate" hreflang="x-default" href="https://qumo.io/about">`
|
||||
|
||||
#### Scenario: NL page hreflang output
|
||||
- **WHEN** a page passes `locale="nl"`, `canonicalUrl="https://qumo.io/nl/about"`, `alternateUrl="https://qumo.io/about"`
|
||||
- **THEN** the head SHALL contain the same three hreflang tags with EN/NL values swapped appropriately, and `x-default` SHALL always point to the EN URL (the `alternateUrl` on NL pages)
|
||||
|
||||
### Requirement: BaseLayout supports JSON-LD injection via prop
|
||||
When the `jsonLd` prop is provided, BaseLayout SHALL render a `<script type="application/ld+json">` tag in `<head>` containing the JSON-serialized value.
|
||||
|
||||
#### Scenario: JSON-LD rendered from prop
|
||||
- **WHEN** `jsonLd={{ "@context": "https://schema.org", "@type": "WebPage", "name": "About" }}` is passed
|
||||
- **THEN** the head SHALL contain `<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","name":"About"}</script>`
|
||||
|
||||
#### Scenario: No JSON-LD script when prop omitted
|
||||
- **WHEN** `jsonLd` prop is not provided
|
||||
- **THEN** no `<script type="application/ld+json">` tag SHALL appear in the rendered head (beyond any injected via the named slot)
|
||||
|
||||
### Requirement: BaseLayout supports additional JSON-LD via named slot
|
||||
A `<slot name="jsonld" />` SHALL be provided inside `<head>` for pages that need additional or multiple structured data blocks beyond the `jsonLd` prop.
|
||||
|
||||
#### Scenario: Named slot accepts additional script
|
||||
- **WHEN** a page passes `<script slot="jsonld" type="application/ld+json">{"@type":"BreadcrumbList"}</script>`
|
||||
- **THEN** that script tag SHALL appear in the rendered `<head>`
|
||||
|
||||
### Requirement: BaseLayout includes Nav and Footer stub components
|
||||
`BaseLayout.astro` SHALL import `src/components/Nav.astro` and `src/components/Footer.astro` and render them at the top and bottom of `<body>` respectively. Both components are stubs that render nothing in this step.
|
||||
|
||||
#### Scenario: Nav and Footer positions in body
|
||||
- **WHEN** any page uses BaseLayout
|
||||
- **THEN** the `<body>` structure SHALL be: Nav stub output → `<slot />` (page content) → Footer stub output
|
||||
|
||||
### Requirement: BaseLayout content slot renders page content
|
||||
A default `<slot />` SHALL be provided between Nav and Footer for page-specific content.
|
||||
|
||||
#### Scenario: Page content appears in slot
|
||||
- **WHEN** a page places markup inside `<BaseLayout>...</BaseLayout>`
|
||||
- **THEN** that markup SHALL appear in the rendered body between Nav and Footer
|
||||
Reference in New Issue
Block a user