step 002 complete

This commit is contained in:
2026-04-10 18:41:58 +02:00
parent e9aa6f452b
commit 089dbaa606
26 changed files with 2732 additions and 15 deletions

View 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

View File

@@ -0,0 +1,87 @@
# Spec: Brand Tokens
## Purpose
Defines the Qumo brand design tokens (colors, typography, fonts) exposed as Tailwind CSS utilities and loaded globally across all pages. Covers color token configuration, Archia font-face declarations, global CSS import strategy, sitemap integration, and the initial brand smoke test page.
## Requirements
### Requirement: Brand color tokens available as Tailwind utilities
The build system SHALL expose Qumo brand colors as Tailwind utility classes via the `@theme` block in `global.css`. The following tokens SHALL be defined:
- `--color-midnight: #102022``bg-midnight`, `text-midnight`, `border-midnight`
- `--color-snow: #F3F3F3``bg-snow`, `text-snow`, `border-snow`
- `--color-brand-blue: #5257E4``bg-brand-blue`, `text-brand-blue`
- `--color-brand-red: #F71E3E``bg-brand-red`, `text-brand-red`
#### Scenario: Color utilities compile correctly
- **WHEN** a component uses `bg-midnight text-snow`
- **THEN** the compiled CSS contains the exact hex values `#102022` and `#F3F3F3`
#### Scenario: Gradient is available as a CSS variable
- **WHEN** a component applies `style="background: var(--gradient-brand)"`
- **THEN** the element renders a linear gradient from `#5257E4` to `#F71E3E`
---
### Requirement: Archia font available as a Tailwind utility
The build system SHALL expose `font-archia` as a Tailwind font-family utility via `--font-archia` in the `@theme` block. The font stack SHALL be `"Archia", ui-sans-serif, system-ui, sans-serif`.
#### Scenario: Font utility applies correct stack
- **WHEN** a component uses `font-archia`
- **THEN** the compiled CSS sets `font-family: "Archia", ui-sans-serif, system-ui, sans-serif`
---
### Requirement: All 6 Archia weights loaded via @font-face
`global.css` SHALL declare `@font-face` rules for all 6 Archia weights using the woff2 files in `src/assets/fonts/archia/`. Each declaration SHALL use `font-display: swap`.
| Weight name | CSS font-weight | File |
|-------------|-----------------|-------------------------------|
| Thin | 100 | archia-thin-webfont.woff2 |
| Light | 300 | archia-light-webfont.woff2 |
| Regular | 400 | archia-regular-webfont.woff2 |
| Medium | 500 | archia-medium-webfont.woff2 |
| SemiBold | 600 | archia-semibold-webfont.woff2 |
| Bold | 700 | archia-bold-webfont.woff2 |
#### Scenario: Font renders with correct weight
- **WHEN** a component uses `font-archia font-bold`
- **THEN** the browser loads `archia-bold-webfont.woff2` and renders text at weight 700
#### Scenario: Fallback renders while font loads
- **WHEN** the woff2 file has not yet loaded
- **THEN** the browser renders text using the system fallback font (`ui-sans-serif`) due to `font-display: swap`
---
### Requirement: global.css imported globally by Astro
`src/styles/global.css` SHALL be imported in `astro.config.mjs` or in the root layout so that brand tokens and font-face declarations apply to every page.
#### Scenario: Tokens available without per-page import
- **WHEN** any `.astro` page component uses `bg-midnight`
- **THEN** the class applies without that page importing `global.css` directly
---
### Requirement: Sitemap integration configured
`astro.config.mjs` SHALL include `@astrojs/sitemap` with `site: 'https://qumo.io'` so that `sitemap-index.xml` is generated on every build.
#### Scenario: Sitemap generated on build
- **WHEN** `npm run build` is executed
- **THEN** `dist/sitemap-index.xml` and `dist/sitemap-0.xml` are present in the output
---
### Requirement: Brand smoke test page
`src/pages/index.astro` SHALL be a temporary visual smoke test page (not production content) that allows a developer to verify all brand tokens render correctly. It SHALL show:
- A color swatch for each brand color token (Midnight, Snow, Brand Blue, Brand Red)
- A gradient bar using `--gradient-brand`
- A font weight sample row for each of the 6 Archia weights (showing uppercase and sentence-case text)
#### Scenario: Smoke test builds without errors
- **WHEN** `npm run build` is run after setup
- **THEN** the build succeeds with zero errors
#### Scenario: All font weights visible in dev
- **WHEN** `npm run dev` is run and the index page is opened
- **THEN** 6 distinct font weights are visible and the gradient bar shows the blue-to-red gradient

View File

@@ -0,0 +1,42 @@
## Purpose
Defines the content localisation system for the Qumo website, including the JSON content files structure and the `i18n.ts` helper that provides locale-appropriate content to pages and components.
## Requirements
### Requirement: Locale content JSON files contain global meta fields
`src/content/en.json` and `src/content/nl.json` SHALL each contain a top-level `"meta"` key with the following fields:
- `siteName: string` — the site name (e.g., `"Qumo"`)
- `defaultTitle: string` — default page title (used as fallback)
- `defaultDescription: string` — default meta description (used as fallback)
- `defaultOgImage: string` — default OG image path; may be empty string initially
#### Scenario: en.json has correct meta structure
- **WHEN** `src/content/en.json` is parsed as JSON
- **THEN** it SHALL have a `meta` object with string values for `siteName`, `defaultTitle`, `defaultDescription`, and `defaultOgImage`
#### Scenario: nl.json has correct meta structure
- **WHEN** `src/content/nl.json` is parsed as JSON
- **THEN** it SHALL have the same `meta` object shape as `en.json`, with Dutch-language values for `defaultTitle` and `defaultDescription`
### Requirement: i18n helper returns correct content for locale
`src/content/i18n.ts` SHALL export a function `getContent(locale: string)` that returns the parsed content object for the given locale. It SHALL return the EN content object for any unrecognized locale string.
#### Scenario: Returns EN content for "en" locale
- **WHEN** `getContent("en")` is called
- **THEN** the function SHALL return the parsed contents of `src/content/en.json`
#### Scenario: Returns NL content for "nl" locale
- **WHEN** `getContent("nl")` is called
- **THEN** the function SHALL return the parsed contents of `src/content/nl.json`
#### Scenario: Falls back to EN for unknown locale
- **WHEN** `getContent("fr")` or any unrecognized locale string is called
- **THEN** the function SHALL return the parsed contents of `src/content/en.json`
### Requirement: Content files use static imports (no runtime fetch)
The `i18n.ts` helper SHALL use static ES module imports (`import enContent from './en.json'`) rather than dynamic `fetch` or `fs.readFile`. This ensures type safety and tree-shaking at build time.
#### Scenario: Build succeeds with static imports
- **WHEN** `npm run build` is executed in the `website/` directory
- **THEN** the build SHALL complete without errors, with locale content bundled statically