# ROUTING_SPEC.md

## 1. Canonical route table (the EN↔NO contract)

| Page | EN (default, root) | NO (`/no` prefix, localised slugs) |
|---|---|---|
| Home | `/` | `/no/` |
| Projects index | `/projects/` | `/no/prosjekter/` |
| JobTrack case study | `/projects/jobtrack/` | `/no/prosjekter/jobtrack/` |
| InboxIntel case study | `/projects/inboxintel/` | `/no/prosjekter/inboxintel/` |
| Homelab capability page | `/projects/homelab/` | `/no/prosjekter/hjemmelab/` |
| Experience | `/experience/` | `/no/erfaring/` |
| About | `/about/` | `/no/om-meg/` |
| Contact | `/contact/` | `/no/kontakt/` |
| CV hub | `/cv/` | `/no/cv/` |
| Site meta ("how this site works") | `/colophon/` | `/no/kolofon/` |
| 404 | `/404` (bilingual page) | — |

Rules: trailing-slash canonical (nginx redirects the bare form, 301). Product names (jobtrack, inboxintel) stay untranslated per CONTENT_STRATEGY glossary. This table lives in code as the single **slug map module** (`i18n/slugMap.ts` conceptually): `pageId → {en: path, no: path}`; consumed by the language switch, hreflang generation, sitemap, and nav. CI test asserts bijection (every EN path has exactly one NO twin and vice versa) and that every content entry maps to a route.

## 2. Non-page routes

| Route | Serves | Notes |
|---|---|---|
| `/cv/connor-babbington-cv-en.pdf` | English CV | Stable, shareable, versioned in repo |
| `/cv/connor-babbington-cv-no.pdf` | Norwegian CV | Same |
| `/api/contact` | POST → contact-relay container | Only non-static route; nginx proxies. 405 on GET |
| `/sitemap-index.xml`, `/robots.txt` | build artifacts | SEO_SPEC |
| `/og/*.png` | Pre-generated OG images per page per locale | Build-time generated |
| `/Linkedin` | 301 → LinkedIn profile | Preserves the existing `cesnimda.co.uk/Linkedin` redirect used on the printed/PDF CV — must not break |
| Legacy WP URLs (`/feed/`, `/wp-*`, old anchors) | 410 Gone (crawler cleanup) except any URL known to be shared → 301 to nearest new page | Inventory old URLs before cutover (Phase 3 task) |

## 3. Redirect & language behaviour

- **No automatic locale redirect.** `/` is always EN. Rationale (Phase 1): shared links must be stable; recruiters forward URLs across languages; auto-redirects break back-button and SEO.
- **First-visit hint:** if `navigator.language` starts with `nb`/`nn`/`no`, and no stored preference, and current locale is EN → show the dismissible hint bar (client-side only, one-time, stores dismissal). Never shown on NO pages, never a redirect.
- **Language switch:** header control resolves current pageId via the slug map → navigates to the twin URL. Stores preference (used only for the hint logic and CV-button default — never for redirects).
- **Unknown `/no/...` path:** bilingual 404 (NO strings first when under `/no/`).

## 4. URL quality rules

- Lowercase, hyphenated, ASCII slugs (NO slugs transliterate: `om-meg`, not `om-mæg` — æøå avoided in URLs for shareability/encoding robustness).
- No query parameters anywhere in canonical URLs; anchors (`#architecture` / `#arkitektur`) are localised to match section headings' explicit ids from the content model (stable across copy edits).
- Case-study section anchors are part of the content schema (DATA_MODEL) so the mini-TOC, deep links, and language-switch section-mapping (J5) stay in sync.

## 5. Navigation state mapping

- `aria-current="page"` from exact pageId match; Projects nav item also marked current on case-study pages (section-current, underline 60% per Phase 1).
- Homepage scroll-spy: section ids `#skills` `#projects` `#experience` `#about` `#contact` (EN) / `#kompetanse` `#prosjekter` `#erfaring` `#om-meg` `#kontakt` (NO) — defined in the slug map's anchor table, not ad hoc.
