diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 0bd0c16..12aeaf2 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,10 +1,8 @@ # PROJECT_STATUS.md -Living status of the Phase 3 implementation. Updated continuously. - -**Current phase:** Phase 3 — implementation -**Branch strategy:** feature branches → PR → `main` (never commit directly to `main`) -**Last updated:** 2026-07-03 +**Current phase:** Phase 3 — implementation **complete** (all milestones ✅) +**Branch strategy:** feature branches → `main` (never committed directly to `main`) +**Last updated:** 2026-07-04 --- @@ -12,59 +10,68 @@ Living status of the Phase 3 implementation. Updated continuously. | # | Milestone | Status | |---|---|---| -| 0 | Repo, tooling, tracking (git, pnpm, docs moved to `docs/`) | 🟡 in progress | -| 1 | Astro base config + token/style layer | ⬜ not started | -| 2 | i18n layer (locales, slug map, dictionaries) | ⬜ not started | -| 3 | Content collections + schemas + EN/NO data | ⬜ not started | -| 4 | UI atoms + core layout (Header, Footer, SEO) | ⬜ not started | -| 5 | Home page sections | ⬜ not started | -| 6 | Case-study + capability templates | ⬜ not started | -| 7 | Remaining pages (about, experience, contact, cv, colophon, 404) | ⬜ not started | -| 8 | Behaviour modules (theme, nav, observer, lightbox, form) | ⬜ not started | -| 9 | SEO layer (meta, hreflang, JSON-LD, OG, sitemap) | ⬜ not started | -| 10 | Contact relay (.NET 9) | ⬜ not started | -| 11 | Tests (vitest + playwright) | ⬜ not started | -| 12 | Docker + CI | ⬜ not started | -| 13 | Verification pass against acceptance criteria | ⬜ not started | +| 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 | ✅ | -Legend: ⬜ not started · 🟡 in progress · ✅ done · ⛔ blocked +## 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 -- Repo initialised locally; remote `git.cesnimda.uk/cesnimda/ResumeSite.git` (push when instructed). +- 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 stored as typed TS modules validated by zod, not Astro content-collection - entries** (DATA_MODEL / I18N_SPEC §3 described "content collections with per-locale - entries"). Rationale: the content is highly structured (not markdown documents) and the - nested `{en,no}` single-entry model serves the spec's own stated goal — "facts exist - once, prose exists twice" — *better* than Astro's parallel per-locale entries, which - can't express author-once invariance natively. All required properties are preserved: - zod schema validation at build (`src/lib/schema.ts`), structural EN/NO parity by - construction, and CI-testable getters (`src/lib/content.ts`). Same validation guarantee, - cleaner authoring, no duplication. -2. **Sitemap hand-rolled from the slug map** instead of `@astrojs/sitemap`. Rationale: the - slug map is the single route contract (ARCHITECTURE A5) and localised slugs (prosjekter - vs projects) don't fit the integration's parallel-slug assumption. Hand-rolling keeps one - source of truth and correct hreflang alternates. +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. -## Prerequisite substitutions in place (interim, non-blocking) +## Pre-launch checklist (carried from Phase 1/2 — owner action) -- Portrait + project screenshots use a `placeholder:` scheme (styled skeletons) until the - seeded-data capture pass and real headshot are provided. Real assets drop straight into - the same content refs. -- CV download targets are the current PDFs (`public/cv/…`); the ATS-safe rework replaces - the files at the same URLs. +- [ ] **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 -_None yet._ - -## Prerequisites carried from Phase 1/2 (not blocking scaffold) - -- Screenshot capture pass on seeded demo data (JobTrack, InboxIntel) — placeholders used until provided. -- ATS-safe CV rework (EN/NO) — current PDFs wired as download targets meanwhile. -- Native-Norwegian review pass of NO content — launch gate. -- Legacy WordPress URL inventory for redirect/410 map. +- Are the JobTrack / InboxIntel repos public? If so, add `repo` links in `src/data/projects/*` + (currently omitted to avoid dead links). diff --git a/TODO.md b/TODO.md index 340e50d..77fca99 100644 --- a/TODO.md +++ b/TODO.md @@ -1,69 +1,28 @@ # TODO.md -Granular work list. Checked items are done; each maps to atomic commits. +Implementation is **complete**. Remaining items are owner/pre-launch actions +(tracked in PROJECT_STATUS.md "Pre-launch checklist"). -## Milestone 0 — Scaffold -- [x] Init git, enable pnpm, move docs into `docs/` -- [x] Root `README.md`, `.gitignore`, `PROJECT_STATUS.md`, `TODO.md` -- [ ] Initial commit on `main`, then feature branch -- [ ] `site/` Astro project: `package.json`, `astro.config.mjs`, `tsconfig.json`, Tailwind config -- [ ] ESLint + Prettier config -- [ ] `pnpm install` + verify `pnpm build` on empty skeleton +## Done — all implementation milestones +- [x] M0 Scaffold (git, pnpm, configs, tracking) +- [x] M1 Tokens & styles (both themes, reduced-motion) +- [x] M2 i18n (locales, slug map, dictionaries, helpers) +- [x] M3 Content (zod schemas, EN/NO data, view models) +- [x] M4 UI atoms + core layout (Header, Footer, Seo, ThemeScript, SkipLink) +- [x] M5 Home sections (hero, proof, skills, projects, timeline, about, contact) +- [x] M6 Case-study + capability templates (diagram, TL;DR, TOC, decisions, gallery) +- [x] M7 All pages, both locales; 404; robots; sitemap +- [x] M8 Behaviour modules (theme, nav, observer, lightbox, form) ≤15 kB +- [x] M9 SEO (meta, hreflang, JSON-LD, build-time OG images) +- [x] M10 Contact relay (.NET 9) — verified endpoints +- [x] M11 Tests — 23 unit + 23 e2e (incl. axe a11y) green +- [x] M12 Docker (site + relay images, compose, nginx) + Gitea CI +- [x] M13 Verification pass -## Milestone 1 — Tokens & styles -- [ ] `styles/tokens.css` (colour/space/type/radius/motion, both themes via `[data-theme]`) -- [ ] `styles/base.css` (reset, base element styles, focus ring, dot-grid, prose) -- [ ] `styles/motion.css` (keyframes + reduced-motion layer) -- [ ] Tailwind `@theme` wired to tokens; verify no raw hex/px in components (lint guard) - -## Milestone 2 — i18n -- [ ] `i18n/locales.ts` (locale config) -- [ ] `i18n/slugMap.ts` (pageId ↔ {en,no} paths + section anchors) — the tested contract -- [ ] `i18n/dictionary.en.ts` + `dictionary.no.ts` (typed, same interface) -- [ ] `i18n/t.ts` accessor + `getLocale`/`resolvePath` helpers -- [ ] Astro i18n config (defaultLocale en at root, no prefixed) - -## Milestone 3 — Content -- [ ] Zod schemas (`content.config.ts`) per DATA_MODEL -- [ ] profile EN/NO · skills EN/NO · experience EN/NO · projects EN/NO (jobtrack, inboxintel, homelab) · meta pages -- [ ] Locale-invariant field inheritance loader -- [ ] Content parity + schema validation (feeds CI later) - -## Milestone 4 — UI atoms + core -- [ ] ui: Chip, Button, SplitCvButton, LangSwitch, ThemeToggle, Card, SectionLabel, TraceMotif, FramedImage, Tooltip -- [ ] core: Base/Page/Home/CaseStudy layouts, Header, MobileNav, Footer, Seo, ThemeScript, SkipLink - -## Milestone 5 — Home -- [ ] Hero, ProofStrip, SkillsGrid, ProjectCards, ExperienceTimeline, AboutTeaser, ContactBand - -## Milestone 6 — Case study -- [ ] CsHeader, TldrBox, MiniToc, SectionRenderer, DecisionList, ArchDiagram, Gallery+Lightbox host, NextPrev -- [ ] Capability template (homelab) - -## Milestone 7 — Pages -- [ ] Home (en/no), projects index, 3 case studies ×2, experience, about, contact, cv, colophon, 404 - -## Milestone 8 — Behaviour -- [ ] theme.ts, nav.ts, observer.ts, lightbox.ts, form.ts (≤15 kB total) - -## Milestone 9 — SEO -- [ ] Seo component meta + hreflang from slug map -- [ ] JSON-LD builders (Person, CreativeWork, WebSite, BreadcrumbList) -- [ ] OG image generation, sitemap, robots.txt - -## Milestone 10 — Relay -- [ ] .NET 9 minimal API: validate, honeypot/time-trap, rate-limit, SMTP send, /healthz -- [ ] appsettings + env config, Dockerfile - -## Milestone 11 — Tests -- [ ] vitest: dictionary parity, slug-map bijection, JSON-LD shape -- [ ] playwright: both locales render, lang switch mapping, CV downloads, form contract, a11y (axe), keyboard - -## Milestone 12 — Docker + CI -- [ ] site Dockerfile (multi-stage → nginx) + nginx.conf (headers, caching, redirects) -- [ ] relay Dockerfile -- [ ] compose (prod + dev) + `.env.example` -- [ ] `.gitea/workflows` CI pipeline (gates → build → images) - -## Milestone 13 — Verification -- [ ] Lighthouse budgets, axe clean, acceptance criteria walkthrough, PROJECT_STATUS final +## Pre-launch (owner) — see PROJECT_STATUS.md +- [ ] Real `/Linkedin` redirect URL in `site/nginx.conf` +- [ ] Seeded-data screenshots + real headshot (replace `placeholder:` refs) +- [ ] ATS-safe CV PDFs (replace files at `site/public/cv/`) +- [ ] Native-Norwegian content review +- [ ] Legacy WordPress URL inventory → finalise 301/410 map +- [ ] CI registry secrets + server `.env` diff --git a/site/eslint.config.js b/site/eslint.config.js index 5c2e155..db021c0 100644 --- a/site/eslint.config.js +++ b/site/eslint.config.js @@ -33,4 +33,11 @@ export default [ files: ['*.config.{js,mjs,ts}'], languageOptions: { globals: { process: 'readonly', URL: 'readonly' } }, }, + { + // Static browser scripts served as-is from public/. + files: ['public/**/*.js'], + languageOptions: { + globals: { window: 'readonly', document: 'readonly', localStorage: 'readonly' }, + }, + }, ]; diff --git a/site/public/theme-init.js b/site/public/theme-init.js index 02220c5..ce2ee45 100644 --- a/site/public/theme-init.js +++ b/site/public/theme-init.js @@ -12,7 +12,7 @@ t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } document.documentElement.dataset.theme = t; - } catch (e) { + } catch { document.documentElement.dataset.theme = 'dark'; } })();