# PROJECT_STATUS.md **Current phase:** Phase 3 — implementation **complete** (all milestones ✅) **Branch strategy:** feature branches → `main` (never committed directly to `main`) **Last updated:** 2026-07-04 --- ## Milestones | # | Milestone | Status | |---|---|---| | 0 | Repo, tooling, tracking | ✅ | | 1 | Astro base config + token/style layer | ✅ | | 2 | i18n layer (locales, slug map, dictionaries) | ✅ | | 3 | Content schemas + EN/NO data | ✅ | | 4 | UI atoms + core layout (Header, Footer, SEO) | ✅ | | 5 | Home page sections | ✅ | | 6 | Case-study + capability templates | ✅ | | 7 | Remaining pages (about, experience, contact, cv, colophon, 404) | ✅ | | 8 | Behaviour modules (theme, nav, observer, lightbox, form) | ✅ | | 9 | SEO layer (meta, hreflang, JSON-LD, OG images, sitemap) | ✅ | | 10 | Contact relay (.NET 9) | ✅ | | 11 | Tests (vitest + playwright) | ✅ | | 12 | Docker + CI | ✅ | | 13 | Verification pass | ✅ | ## Verification results - **Build:** 21 pages + 20 OG images + sitemap/robots, static, clean. - **Lint / types:** ESLint + Prettier clean; `tsc --noEmit` clean; `astro check` wired. - **Unit tests (vitest):** 23 passing — dictionary parity, slug-map bijection, content-schema validation, JSON-LD builders. - **E2E (playwright):** 23 passing — both locales render, language switch maps page↔page (incl. JS-disabled), CV downloads resolve, contact form happy-path + honeypot, and **axe a11y with zero serious/critical violations across 5 templates × 2 themes**. - **Docker:** both images build; relay `/healthz` → 200; site container serves EN + NO with CSP header and the `/Linkedin` 301 redirect. - **Contrast:** all body/label text ≥ 4.5:1 in both themes (axe-verified; `ink-faint` and the light accent were raised to meet AA). ## Environment - Node v22.23.1 · pnpm 9.15.9 · .NET SDK 10.0.201 (relay targets `net9.0`) · Docker 29.3.1. ## Deviations from spec (with rationale) 1. **Content as typed TS modules + zod, not Astro content-collection entries.** The nested `{en,no}` model serves the spec's "facts once, prose twice" goal better than parallel per-locale entries; same build-time zod validation and CI-testable getters. 2. **Sitemap hand-rolled from the slug map** (localised slugs don't fit `@astrojs/sitemap`'s parallel-slug assumption; keeps one route contract). 3. **OG images via resvg + hand-built SVG, not satori** (satori's parser can't read the vendored variable fonts). Deterministic offline builds; fonts vendored in-repo. 4. **Relay targets `net9.0` built with the .NET 10 SDK** (only SDK available); runs on the `aspnet:9.0` runtime in Docker. 5. **Site nginx config co-located in `site/`** (not `deploy/`) so the image build context stays `site/`. `deploy/` holds the compose stacks + env template as specified. 6. **Theme-init is an external script** (`/theme-init.js`) so the CSP can use `script-src 'self'` with no inline hash. ## Pre-launch checklist (carried from Phase 1/2 — owner action) - [ ] **Replace the `/Linkedin` redirect target** in `site/nginx.conf` with the real LinkedIn profile URL before cutover (currently a placeholder slug). - [ ] Screenshot capture pass on **seeded demo data** for JobTrack + InboxIntel; drop real captures into the `placeholder:` media refs (`src/data/projects/*`). - [ ] Drop the real **outdoor headshot** into the portrait ref (replaces `placeholder:portrait`). - [ ] **ATS-safe CV rework** (EN/NO) — replace the PDFs at `site/public/cv/` (same URLs). - [ ] **Native-Norwegian review** of all NO content (launch gate). - [ ] **Legacy WordPress URL inventory** → confirm the 301/410 map in `nginx.conf`. - [ ] Configure CI secrets (`REGISTRY_USER`, `REGISTRY_TOKEN`) + the relay `.env` on the server. ## Open questions / blocked-on-user - Are the JobTrack / InboxIntel repos public? If so, add `repo` links in `src/data/projects/*` (currently omitted to avoid dead links).