Files
cesnimda bfa8cfd357 feat: homelab case study from live infra; Traefik-based deploy
- rewrote homelab (content + topology diagram + stack) from a live inspection of the host:
  Ubuntu 24.04, ~30 Docker services behind Traefik (Cloudflare-fronted, TLS, HTTP/3),
  Authentik SSO forward-auth, CrowdSec, Pi-hole, self-hosted Gitea + CI runner, socket-proxy,
  per-app network isolation; WordPress framed as being decommissioned (not future arch)
- deploy: docker-compose now uses Traefik labels + traefik_proxy network (was assumed nginx
  edge); .env.example adds SITE_HOST/TRAEFIK_ENTRYPOINT; colophon + ARCHITECTURE/DOCKER/
  DEPLOYMENT specs corrected nginx-edge -> Traefik (site container still serves via nginx)
- PROJECT_STATUS: pre-launch checklist updated; infra section added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:23:09 +02:00

8.5 KiB

ARCHITECTURE.md

System architecture for the bilingual portfolio. Companion detail specs: ROUTING_SPEC, COMPONENT_ARCHITECTURE, I18N_SPEC, DATA_MODEL, DOCKER_SPEC, DEPLOYMENT_SPEC.


1. System overview

                        ┌──────────────────────────── Connor's server ───────────────────────────┐
 Browser ── HTTPS ──►   │  Traefik (existing host reverse proxy, TLS; Cloudflare-fronted)        │
                        │    ├── cesnimda.co.uk/            → [site] nginx container (static)    │
                        │    ├── cesnimda.co.uk/api/contact → [contact-relay] .NET 9 container   │
                        │    └── git.cesnimda.uk            → Gitea (existing)                   │
                        │                                                                        │
                        │  docker compose stack "resumesite": site + contact-relay (+ optional   │
                        │  analytics slot)                                                       │
                        └────────────────────────────────────────────────────────────────────────┘

 Build time (CI, Gitea Actions):
   content (markdown/JSON, images) ─► Astro build ─► static dist/ ─► site image (nginx + dist)
                                                     └► budgets/tests gate the publish

Architectural style: static-first with one stateless dynamic sidecar. All pages are pre-rendered at build time (SSG). There is no runtime rendering, no database, no sessions. The only mutable runtime path is POST /api/contact, isolated in its own container so the site itself has zero attack surface beyond static file serving.

2. Repository layout (single repo: ResumeSite on git.cesnimda.uk)

ResumeSite/
├── site/                        # Astro project
│   ├── src/
│   │   ├── pages/               # route files (EN root) — see ROUTING_SPEC
│   │   │   └── no/              # Norwegian route tree (localised slugs)
│   │   ├── layouts/             # Base, Page, CaseStudy layouts
│   │   ├── components/          # see COMPONENT_ARCHITECTURE
│   │   │   ├── core/            # Header, Footer, Seo, ThemeScript, SkipLink
│   │   │   ├── home/            # Hero, ProofStrip, SkillsGrid, ProjectCards, Timeline, ContactBand
│   │   │   ├── case-study/      # TldrBox, MiniToc, DecisionList, Diagram, Gallery
│   │   │   └── ui/              # Chip, Button, SplitButton, LangSwitch, ThemeToggle, Card
│   │   ├── content/             # content collections (the bilingual data layer)
│   │   │   ├── projects/{en,no}/...
│   │   │   ├── experience/{en,no}/...
│   │   │   └── profile/{en,no}.json
│   │   ├── i18n/                # locales.ts, dictionary.{en,no}.ts, slugMap.ts, t() helper
│   │   ├── styles/              # tokens.css (design tokens), base.css, motion.css
│   │   ├── scripts/             # island helpers: observer.ts, lightbox.ts, nav.ts, form.ts, theme.ts
│   │   └── assets/              # source images, diagrams (SVG), fonts
│   ├── public/                  # CVs (pdf), favicon, robots.txt
│   └── (astro/tailwind/ts configs)
├── relay/                       # .NET 9 minimal API contact relay (own Dockerfile)
├── deploy/                      # compose files, nginx site config, env templates
├── .gitea/workflows/            # CI/CD pipelines
├── docs/                        # this spec set + phase-1 design (moved into repo at Phase 3 start)
└── tests/                       # playwright e2e (vitest lives beside site/src)

Rationale: one repo keeps content, design docs, infra and both services versioned together — appropriate for a single-owner product and makes the repo itself portfolio evidence.

3. Layered architecture of the site

┌ Content layer      markdown + JSON content collections, schema-validated (zod) — DATA_MODEL.md
├ i18n layer         locale config, slug map, typed dictionaries — I18N_SPEC.md
├ Template layer     layouts + components consume {content, locale} → HTML — COMPONENT_ARCHITECTURE.md
├ Token layer        CSS custom properties (themes) + Tailwind utilities
├ Behaviour layer    5 small TS modules (theme, nav, observer, lightbox, form) as progressive enhancement
└ Delivery layer     static dist/ in nginx container; immutable-hashed assets; long-cache headers

Dependency rule (enforced by review, mirrors the projects' clean-architecture story): templates depend on content+i18n; content depends on nothing; behaviour modules depend on DOM contracts (data-attributes), never on content. No component reads raw filesystem or hardcodes strings.

4. Key architectural decisions (ADR summary)

# Decision Alternative rejected Why
A1 Full SSG, no SSR Astro SSR/hybrid No per-request data exists; SSR adds a Node runtime, cold-start surface, and ops burden for zero benefit
A2 Contact relay as separate container SSR endpoint in site / SaaS form Keeps site image immutable+static; isolates the only untrusted-input path; self-host principle
A3 Content collections in-repo Headless CMS (Directus/Strapi/…) Single author, git workflow is his CMS; removes a service, a database, and an attack surface
A4 CSS custom-property tokens, data-theme attribute switch Tailwind dark: variants only One token source drives both themes + allows no-flash inline theme script + inspectable tokens
A5 Localised slugs via explicit map module File-name convention magic The EN↔NO page mapping is a contract used by header switch, hreflang, sitemap — must be a single typed artifact testable in CI
A6 Behaviour as data-attribute modules Framework islands JS budget < 40 kB; three behaviours don't justify a runtime; escape hatch to Preact pre-authorised if one outgrows it
A7 Images processed at build Runtime image service Static hosting; fixed dimensions guarantee CLS 0; AVIF/WebP generated once
A8 CVs as versioned static assets in public/ with stable URLs (/cv/connor-babbington-cv-en.pdf) External drive links Stable recruiter-shareable URLs; git-versioned; locale-matched download logic trivial

5. Performance strategy (architecture-level)

  • Budget ownership: budgets live in CI (TECH_SPEC §9); architecture guarantees they're achievable: no runtime framework, fonts subset + preloaded, hero has no image dependency for LCP (LCP element = H1 text), screenshots below fold lazy + aspect-locked.
  • Caching: hashed asset filenames → Cache-Control: immutable, 1y; HTML no-cache (revalidate) so deploys are instant; CV PDFs short cache (they update).
  • Critical path: inline theme-init script (tiny, blocking by design to prevent theme flash) + single CSS file per page; JS deferred entirely.
  • Third-party requests: zero. No CDN fonts, no analytics beacons (unless self-hosted later), no embeds. This is both performance and the GDPR/no-cookie-banner stance.

6. Security posture

  • Static site: attack surface = nginx serving files. Headers set at the site-nginx layer: CSP (no inline scripts except the hashed theme script, no external origins), HSTS (at the edge proxy), X-Content-Type-Options, Referrer-Policy, Permissions-Policy minimal.
  • Relay: input validation + size caps, honeypot + minimum-fill-time trap, per-IP rate limit, SMTP credentials via env/secret (never in image), no persistence, structured minimal logging (no message bodies), runs as non-root, read-only filesystem.
  • Supply chain: pnpm lockfile committed, Renovate (already familiar from InboxIntel) on the repo, images pinned by digest in compose.

7. Failure modes & degradation

Failure Behaviour
Relay down Form shows the fallback panel (direct e-mail + copy button) — conversion path survives
JS disabled/broken Full content readable; nav works (plain links); no lightbox/reveals — by design (progressive enhancement)
Old cached HTML after deploy HTML revalidates; hashed assets never mismatch
Locale content missing (build-time) CI fails the build (parity test) — never ships a broken language