bfa8cfd357
- 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>
106 lines
8.5 KiB
Markdown
106 lines
8.5 KiB
Markdown
# 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 |
|