step 002 complete
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
## Context
|
||||
|
||||
The Astro project was scaffolded with `astro@6.1.5` and directory structure in place, but `astro.config.mjs` is empty and no Tailwind integration exists. The Archia woff2 font files are already present at `src/assets/fonts/archia/`. The migration steps doc was written expecting Tailwind v3 patterns (`tailwind.config.mjs`); we are using Tailwind v4 instead.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Tailwind v4 fully wired into the Astro 6 build pipeline
|
||||
- All Qumo brand color tokens available as Tailwind utility classes (`bg-midnight`, `text-snow`, etc.)
|
||||
- `font-archia` available as a Tailwind font utility
|
||||
- All 6 Archia weights loaded via `@font-face` with `font-display: swap`
|
||||
- `@astrojs/sitemap` configured with `site: 'https://qumo.io'`
|
||||
- A smoke test page that confirms every token and weight renders correctly
|
||||
|
||||
**Non-Goals:**
|
||||
- No real page content, layout, or navigation (those are steps 002–004)
|
||||
- No NL locale routing yet
|
||||
- No SEO meta tags or structured data yet
|
||||
- No production optimisation of font loading (preloads come in step 002 with BaseLayout)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Tailwind v4 over v3
|
||||
|
||||
**Decision:** Use `@tailwindcss/vite` (Tailwind v4) rather than `tailwindcss@3` + `@astrojs/tailwind`.
|
||||
|
||||
**Rationale:** Tailwind v4 is the current major version and integrates natively with Vite via a single Vite plugin — no Astro integration adapter needed. Configuration is CSS-based (`@theme` block), which keeps brand tokens co-located with the font-face declarations in one file. Astro 6 ships with Vite 6, which is fully supported.
|
||||
|
||||
**Alternative considered:** Tailwind v3 (`tailwind.config.mjs`). Rejected because it requires an additional `@astrojs/tailwind` adapter with limited Astro 6 testing, and the JS config file adds complexity for what is simply a set of color + font token definitions.
|
||||
|
||||
### CSS-based token definition
|
||||
|
||||
**Decision:** Brand tokens defined in `src/styles/global.css` using the `@theme` block, not in a JS config.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-midnight: #102022;
|
||||
--color-snow: #F3F3F3;
|
||||
--color-brand-blue: #5257E4;
|
||||
--color-brand-red: #F71E3E;
|
||||
--font-archia: "Archia", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** Tailwind v4 CSS variables in `@theme` are automatically exposed as utilities (`bg-midnight`, `text-snow`, `font-archia`, etc.) with no additional config. This is the idiomatic v4 pattern.
|
||||
|
||||
### Gradient as CSS custom property, not a Tailwind utility
|
||||
|
||||
**Decision:** Define the brand gradient as a CSS custom property in `global.css` rather than a Tailwind background utility.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--gradient-brand: linear-gradient(135deg, #5257E4, #F71E3E);
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** The gradient is used in specific, controlled contexts (hero accent, CTA sections) — never as a flat background. A raw CSS variable is easier to apply precisely than a generated utility class, and avoids accidentally using the gradient colors as standalone flat colors.
|
||||
|
||||
### Font loading strategy
|
||||
|
||||
**Decision:** `@font-face` declarations in `global.css` with `font-display: swap`. Load Regular, SemiBold, and Bold in the initial stylesheet; defer Thin, Light, and Medium.
|
||||
|
||||
**Rationale:** Archia Regular/SemiBold/Bold covers all visible text in the design. Thin/Light/Medium are used sparingly if at all. Font preloads (`<link rel="preload">`) will be added to BaseLayout in step 002 for the three primary weights.
|
||||
|
||||
### Smoke test in index.astro
|
||||
|
||||
**Decision:** Replace the default `index.astro` with a dev-only smoke test. It is not a real page — it will be fully replaced in the homepage step.
|
||||
|
||||
**Structure:** Color swatch grid (4 swatches + gradient bar) + font weight table (Thin → Bold, each showing uppercase and sentence-case samples) + a note that this is a dev artifact.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Tailwind v4 is newer** → Less community Q&A available. Mitigation: Astro Docs MCP server has current documentation; the API surface for what we need (colors, fonts) is stable.
|
||||
- **`@tailwindcss/typography` compatibility** → Already in devDeps; v4-compatible version should be installed. Mitigation: verify it installs without peer dep errors after `npm install`.
|
||||
- **Smoke test left in place too long** → If step 002 is delayed, the index route shows a dev page. Acceptable: this is a dev-only environment until deployment.
|
||||
@@ -0,0 +1,33 @@
|
||||
## Why
|
||||
|
||||
The Astro project scaffold exists but has no Tailwind CSS integration or brand tokens configured. Before any UI work can begin, the design system foundation must be in place: brand colors, the Archia typeface, and the gradient — all wired into Tailwind so every subsequent step can use utility classes directly.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Install `tailwindcss` and `@tailwindcss/vite` (Tailwind v4) and `@astrojs/sitemap`
|
||||
- Configure `astro.config.mjs` with the Vite Tailwind plugin and sitemap integration
|
||||
- Create `src/styles/global.css` with:
|
||||
- `@import "tailwindcss"`
|
||||
- `@theme` block defining brand color tokens and `font-archia`
|
||||
- `@font-face` declarations for all 6 Archia weights (Thin, Light, Regular, Medium, SemiBold, Bold)
|
||||
- Replace the default `src/pages/index.astro` with a brand smoke test page showing color swatches, font weight samples, and a gradient bar
|
||||
- No `tailwind.config.mjs` — configuration is CSS-based (Tailwind v4 pattern)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `brand-tokens`: Qumo design system tokens available as Tailwind utilities — brand colors (`midnight`, `snow`, `brand-blue`, `brand-red`), `font-archia`, and the gradient definition
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_(none)_
|
||||
|
||||
## Impact
|
||||
|
||||
- `website/package.json` — new dependencies: `tailwindcss`, `@tailwindcss/vite`, `@astrojs/sitemap`
|
||||
- `website/astro.config.mjs` — adds Vite plugin and sitemap integration
|
||||
- `website/src/styles/global.css` — new file, imported globally
|
||||
- `website/src/pages/index.astro` — replaced with temporary smoke test (will be replaced again in a later step when the homepage is built)
|
||||
- No SEO impact — smoke test page is a dev-only visual check
|
||||
- No content strings needed — smoke test has no user-visible text to localize
|
||||
@@ -0,0 +1,81 @@
|
||||
## ADDED 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
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. Install dependencies
|
||||
|
||||
- [x] 1.1 Install `tailwindcss` and `@tailwindcss/vite` in `website/`
|
||||
- [x] 1.2 Install `@astrojs/sitemap` in `website/`
|
||||
|
||||
## 2. Configure Astro
|
||||
|
||||
- [x] 2.1 Update `astro.config.mjs` to add `@tailwindcss/vite` as a Vite plugin and `@astrojs/sitemap` as an integration with `site: 'https://qumo.io'`
|
||||
|
||||
## 3. Create global.css
|
||||
|
||||
- [x] 3.1 Create `src/styles/global.css` with `@import "tailwindcss"`
|
||||
- [x] 3.2 Add `@theme` block with brand color tokens: `--color-midnight`, `--color-snow`, `--color-brand-blue`, `--color-brand-red`
|
||||
- [x] 3.3 Add `--font-archia` to the `@theme` block with system fallback stack
|
||||
- [x] 3.4 Add `:root` block with `--gradient-brand: linear-gradient(135deg, #5257E4, #F71E3E)`
|
||||
- [x] 3.5 Add `@font-face` declarations for all 6 Archia weights (Thin/100, Light/300, Regular/400, Medium/500, SemiBold/600, Bold/700) pointing to `../assets/fonts/archia/` woff2 files, each with `font-display: swap`
|
||||
|
||||
## 4. Wire global.css into Astro
|
||||
|
||||
- [x] 4.1 Import `../styles/global.css` in `src/pages/index.astro` (will move to BaseLayout in step 002)
|
||||
|
||||
## 5. Brand smoke test page
|
||||
|
||||
- [x] 5.1 Replace the default `src/pages/index.astro` content with a smoke test page: a color swatch grid (Midnight, Snow, Brand Blue, Brand Red, + gradient bar) and a font weight table (Thin through Bold, each row showing uppercase + sentence-case sample text)
|
||||
|
||||
## 6. Verify
|
||||
|
||||
- [x] 6.1 Run `npm run build` inside `website/` — must succeed with zero errors
|
||||
- [x] 6.2 Run `npm run dev` and open the index page — verify all 4 color swatches, the gradient bar, and all 6 font weights render correctly
|
||||
- [x] 6.3 Confirm `dist/sitemap-index.xml` is present in the build output
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-10
|
||||
@@ -0,0 +1,79 @@
|
||||
## 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.css` is Tailwind v4 (`@import "tailwindcss"` + `@theme`) — no `tailwind.config.mjs`
|
||||
- Archia `@font-face` declarations already live in `global.css`
|
||||
- `public/` has `favicon.svg` and `favicon.ico`
|
||||
- No `src/layouts/`, `src/components/`, or `src/content/` directories yet
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Single `BaseLayout.astro` that 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.astro` and `Footer.astro` so BaseLayout can import them (filled in steps 003+)
|
||||
- `en.json` / `nl.json` content files + `i18n.ts` loader for global meta fields
|
||||
- `index.astro` updated to use `BaseLayout`
|
||||
|
||||
**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 — `ogImage` prop will be empty string initially)
|
||||
- Any client-side JavaScript
|
||||
- `@astrojs/image` or 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: `ogImage` prop defaults to empty string; the `<meta property="og:image">` tag is only rendered when `ogImage` is 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 placeholder `canonicalUrl` and `alternateUrl` props. Use `"/"` and `"/nl"` as stand-ins. This is fine since the smoke-test page will be replaced when the real homepage is built.
|
||||
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
Step 001 established brand tokens and fonts — we now have a working Astro project but every page must manually construct its own `<html>` document with head meta tags. Step 002 creates the shared `BaseLayout.astro` shell so all subsequent pages start from a consistent, SEO-complete foundation without repetition.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **New**: `src/layouts/BaseLayout.astro` — HTML document shell accepting props for title, description, locale, canonical URL, alternate URL, OG image, and JSON-LD structured data
|
||||
- **New**: `src/components/Nav.astro` — empty stub (filled in step 003)
|
||||
- **New**: `src/components/Footer.astro` — empty stub (filled in a later step)
|
||||
- **New**: `src/content/en.json` — initial structure with global meta fields (site name, default title, default description, default OG image path)
|
||||
- **New**: `src/content/nl.json` — Dutch translations of the same global meta fields
|
||||
- **New**: `src/content/i18n.ts` — helper that loads and returns the correct JSON content object based on locale string
|
||||
- **Update**: `src/pages/index.astro` — replace manual `<html>` shell with `<BaseLayout>`, passing appropriate props
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `base-layout`: HTML document shell providing consistent `<head>` (charset, viewport, favicon, Archia font preloads, canonical link, hreflang alternates, OG tags, Twitter card tags) and `<body>` defaults (Midnight background, Snow text, Archia font), with stub nav/footer and content/JSON-LD slots
|
||||
- `content-i18n`: File-based content loading system — JSON files per locale (`en.json`, `nl.json`) plus an `i18n.ts` helper function that returns the correct content object given a locale string
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `brand-tokens`: No requirement changes — the existing spec covers colors, fonts, and gradient; BaseLayout consumes these tokens but does not change them
|
||||
|
||||
## Impact
|
||||
|
||||
- All future pages (`/`, `/about`, `/ai-launchpad`, `/contact`, `/privacy`, plus `/nl/*` variants) will import and use `BaseLayout`
|
||||
- SEO: adds canonical URL, hreflang (`en` ↔ `nl`), OG tags, and Twitter card tags to every page
|
||||
- Affects `en.json` / `nl.json` content structure (introduces the `meta` key)
|
||||
- No new npm dependencies — uses only Astro built-ins and TypeScript
|
||||
@@ -0,0 +1,76 @@
|
||||
## ADDED 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
|
||||
@@ -0,0 +1,38 @@
|
||||
## ADDED 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
|
||||
@@ -0,0 +1,33 @@
|
||||
## 1. Content Files
|
||||
|
||||
- [x] 1.1 Create `src/content/en.json` with top-level `meta` key: `siteName`, `defaultTitle`, `defaultDescription`, `defaultOgImage` (empty string)
|
||||
- [x] 1.2 Create `src/content/nl.json` with same `meta` shape, Dutch values for `defaultTitle` and `defaultDescription`
|
||||
- [x] 1.3 Create `src/content/i18n.ts` exporting `getContent(locale: string)` using static imports of both JSON files; fall back to EN for unknown locales
|
||||
|
||||
## 2. Stub Components
|
||||
|
||||
- [x] 2.1 Create `src/components/Nav.astro` as an empty stub (renders nothing — filled in step 003)
|
||||
- [x] 2.2 Create `src/components/Footer.astro` as an empty stub (renders nothing — filled in a later step)
|
||||
|
||||
## 3. BaseLayout Component
|
||||
|
||||
- [x] 3.1 Create `src/layouts/BaseLayout.astro` with props: `title`, `description`, `locale` (default `"en"`), `canonicalUrl`, `alternateUrl`, `ogImage` (optional), `jsonLd` (optional `Record<string, unknown>`)
|
||||
- [x] 3.2 Add `<head>` content: `<meta charset="utf-8">`, `<meta name="viewport">`, `<title>`, `<link rel="icon">` for both SVG and ICO favicons
|
||||
- [x] 3.3 Add three Archia font preload tags in `<head>`: Regular (400), SemiBold (600), Bold (700) woff2 files only
|
||||
- [x] 3.4 Add `<link rel="canonical">` and three hreflang `<link rel="alternate">` tags (`en`, `nl`, `x-default`) using `canonicalUrl` and `alternateUrl` props
|
||||
- [x] 3.5 Add OG meta tags (`og:type`, `og:url`, `og:title`, `og:description`, `og:site_name`); add `og:image` only when `ogImage` is truthy
|
||||
- [x] 3.6 Add Twitter card meta tags (`twitter:card`, `twitter:title`, `twitter:description`); add `twitter:image` only when `ogImage` is truthy
|
||||
- [x] 3.7 Add JSON-LD script tag in `<head>` rendered from `jsonLd` prop when provided (`set:html={JSON.stringify(jsonLd)}`)
|
||||
- [x] 3.8 Add `<slot name="jsonld" />` inside `<head>` for additional JSON-LD blocks
|
||||
- [x] 3.9 Add `<body class="bg-midnight text-snow font-archia">` with `<Nav />`, default `<slot />`, and `<Footer />` in order
|
||||
|
||||
## 4. Update Index Page
|
||||
|
||||
- [x] 4.1 Update `src/pages/index.astro` to import and use `BaseLayout`, passing `title`, `description`, `canonicalUrl="https://qumo.io/"`, `alternateUrl="https://qumo.io/nl"`, and `locale="en"`; remove manual `<html>/<head>/<body>` shell
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- [x] 5.1 Run `cd website && npm run build` — build must succeed with zero errors
|
||||
- [x] 5.2 View page source in browser (`npm run dev`): confirm `<head>` contains charset, viewport, title, favicon links, three font preload tags, canonical, three hreflang links, OG tags, Twitter card tags
|
||||
- [x] 5.3 Confirm body has `bg-midnight text-snow font-archia` classes and smoke-test content still renders correctly
|
||||
- [x] 5.4 Confirm no `og:image` or `twitter:image` tag appears (since `ogImage` is not passed)
|
||||
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
|
||||
87
openspec/specs/brand-tokens/spec.md
Normal file
87
openspec/specs/brand-tokens/spec.md
Normal 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
|
||||
42
openspec/specs/content-i18n/spec.md
Normal file
42
openspec/specs/content-i18n/spec.md
Normal 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
|
||||
1354
package-lock.json
generated
Normal file
1354
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"tailwindcss": "^4.2.2"
|
||||
}
|
||||
}
|
||||
410
tmp/page-source.html
Normal file
410
tmp/page-source.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,13 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
export default defineConfig({
|
||||
site: 'https://qumo.io',
|
||||
integrations: [sitemap()],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
|
||||
3
website/src/components/Footer.astro
Normal file
3
website/src/components/Footer.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
// A later step will implement this component
|
||||
---
|
||||
3
website/src/components/Nav.astro
Normal file
3
website/src/components/Nav.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
// Step 003 will implement this component
|
||||
---
|
||||
8
website/src/content/en.json
Normal file
8
website/src/content/en.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"meta": {
|
||||
"siteName": "Qumo",
|
||||
"defaultTitle": "Qumo — Data & AI for Dutch MKB",
|
||||
"defaultDescription": "Qumo helps Dutch mid-market companies turn data into decisions. From BI dashboards to AI-powered workflows, we make data work for your business.",
|
||||
"defaultOgImage": ""
|
||||
}
|
||||
}
|
||||
9
website/src/content/i18n.ts
Normal file
9
website/src/content/i18n.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import enContent from './en.json';
|
||||
import nlContent from './nl.json';
|
||||
|
||||
type Content = typeof enContent;
|
||||
|
||||
export function getContent(locale: string): Content {
|
||||
if (locale === 'nl') return nlContent as Content;
|
||||
return enContent;
|
||||
}
|
||||
8
website/src/content/nl.json
Normal file
8
website/src/content/nl.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"meta": {
|
||||
"siteName": "Qumo",
|
||||
"defaultTitle": "Qumo — Data & AI voor Nederlands MKB",
|
||||
"defaultDescription": "Qumo helpt Nederlandse middelgrote bedrijven data omzetten in beslissingen. Van BI-dashboards tot AI-gedreven workflows — wij laten data werken voor uw bedrijf.",
|
||||
"defaultOgImage": ""
|
||||
}
|
||||
}
|
||||
80
website/src/layouts/BaseLayout.astro
Normal file
80
website/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import Nav from '../components/Nav.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
// Import critical font weights with ?url so preload hrefs match Vite-processed URLs
|
||||
import archiaRegularUrl from '../assets/fonts/archia/archia-regular-webfont.woff2?url';
|
||||
import archiaSemiBoldUrl from '../assets/fonts/archia/archia-semibold-webfont.woff2?url';
|
||||
import archiaBoldUrl from '../assets/fonts/archia/archia-bold-webfont.woff2?url';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
locale?: string;
|
||||
canonicalUrl: string;
|
||||
alternateUrl: string;
|
||||
ogImage?: string;
|
||||
jsonLd?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
locale = 'en',
|
||||
canonicalUrl,
|
||||
alternateUrl,
|
||||
ogImage,
|
||||
jsonLd,
|
||||
} = Astro.props;
|
||||
|
||||
const enUrl = locale === 'en' ? canonicalUrl : alternateUrl;
|
||||
const nlUrl = locale === 'nl' ? canonicalUrl : alternateUrl;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
||||
<!-- Font preloads: Regular, SemiBold, Bold only -->
|
||||
<link rel="preload" href={archiaRegularUrl} as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href={archiaSemiBoldUrl} as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href={archiaBoldUrl} as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<!-- Canonical & hreflang -->
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<link rel="alternate" hreflang="en" href={enUrl} />
|
||||
<link rel="alternate" hreflang="nl" href={nlUrl} />
|
||||
<link rel="alternate" hreflang="x-default" href={enUrl} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:site_name" content="Qumo" />
|
||||
{ogImage && <meta property="og:image" content={ogImage} />}
|
||||
|
||||
<!-- Twitter card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
||||
|
||||
<!-- JSON-LD structured data -->
|
||||
{jsonLd && <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />}
|
||||
<slot name="jsonld" />
|
||||
</head>
|
||||
<body class="bg-midnight text-snow font-archia">
|
||||
<Nav />
|
||||
<slot />
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,17 +1,93 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
const title = 'Qumo — Brand Smoke Test';
|
||||
const description = 'Brand token and font weight smoke test — dev artifact.';
|
||||
---
|
||||
|
||||
---
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={description}
|
||||
locale="en"
|
||||
canonicalUrl="https://qumo.io/"
|
||||
alternateUrl="https://qumo.io/nl"
|
||||
>
|
||||
<main class="p-8 min-h-screen">
|
||||
<h1 class="text-2xl font-bold uppercase mb-8 tracking-widest">Brand Smoke Test</h1>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
</body>
|
||||
</html>
|
||||
<!-- Color Swatches -->
|
||||
<section class="mb-10">
|
||||
<h2 class="text-sm uppercase tracking-widest mb-4 opacity-50">Brand Colors</h2>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-24 h-24 rounded bg-midnight border border-snow/20"></div>
|
||||
<span class="text-xs">Midnight<br/>#102022</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-24 h-24 rounded bg-snow"></div>
|
||||
<span class="text-xs text-snow/70">Snow<br/>#F3F3F3</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-24 h-24 rounded bg-brand-blue"></div>
|
||||
<span class="text-xs">Brand Blue<br/>#5257E4</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-24 h-24 rounded bg-brand-red"></div>
|
||||
<span class="text-xs">Brand Red<br/>#F71E3E</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="w-24 h-24 rounded" style="background: var(--gradient-brand)"></div>
|
||||
<span class="text-xs">Gradient<br/>Blue → Red</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Font Weight Samples -->
|
||||
<section class="mb-10">
|
||||
<h2 class="text-sm uppercase tracking-widest mb-4 opacity-50">Archia Weights</h2>
|
||||
<table class="w-full max-w-2xl border-collapse">
|
||||
<thead>
|
||||
<tr class="text-xs uppercase tracking-widest opacity-50 border-b border-snow/20">
|
||||
<th class="text-left pb-2 pr-8">Weight</th>
|
||||
<th class="text-left pb-2 pr-8">Uppercase</th>
|
||||
<th class="text-left pb-2">Sentence case</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-archia">
|
||||
<tr class="border-b border-snow/10">
|
||||
<td class="py-3 pr-8 text-xs opacity-50">Thin 100</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase" style="font-weight: 100">QUMO DATA</td>
|
||||
<td class="py-3 text-xl" style="font-weight: 100">Qumo data analytics</td>
|
||||
</tr>
|
||||
<tr class="border-b border-snow/10">
|
||||
<td class="py-3 pr-8 text-xs opacity-50">Light 300</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase" style="font-weight: 300">QUMO DATA</td>
|
||||
<td class="py-3 text-xl" style="font-weight: 300">Qumo data analytics</td>
|
||||
</tr>
|
||||
<tr class="border-b border-snow/10">
|
||||
<td class="py-3 pr-8 text-xs opacity-50">Regular 400</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase font-normal">QUMO DATA</td>
|
||||
<td class="py-3 text-xl font-normal">Qumo data analytics</td>
|
||||
</tr>
|
||||
<tr class="border-b border-snow/10">
|
||||
<td class="py-3 pr-8 text-xs opacity-50">Medium 500</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase font-medium">QUMO DATA</td>
|
||||
<td class="py-3 text-xl font-medium">Qumo data analytics</td>
|
||||
</tr>
|
||||
<tr class="border-b border-snow/10">
|
||||
<td class="py-3 pr-8 text-xs opacity-50">SemiBold 600</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase font-semibold">QUMO DATA</td>
|
||||
<td class="py-3 text-xl font-semibold">Qumo data analytics</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-3 pr-8 text-xs opacity-50">Bold 700</td>
|
||||
<td class="py-3 pr-8 text-xl uppercase font-bold">QUMO DATA</td>
|
||||
<td class="py-3 text-xl font-bold">Qumo data analytics</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<p class="text-xs opacity-30 uppercase tracking-widest">Dev artifact — will be replaced by the homepage</p>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
62
website/src/styles/global.css
Normal file
62
website/src/styles/global.css
Normal file
@@ -0,0 +1,62 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-midnight: #102022;
|
||||
--color-snow: #F3F3F3;
|
||||
--color-brand-blue: #5257E4;
|
||||
--color-brand-red: #F71E3E;
|
||||
|
||||
--font-archia: "Archia", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--gradient-brand: linear-gradient(135deg, #5257E4, #F71E3E);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-thin-webfont.woff2") format("woff2");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-light-webfont.woff2") format("woff2");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-regular-webfont.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-medium-webfont.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-semibold-webfont.woff2") format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archia";
|
||||
src: url("../assets/fonts/archia/archia-bold-webfont.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
Reference in New Issue
Block a user