Context & problem
A portfolio can easily become a static brochure: quick to publish, but technically thin. jaridata.dev intentionally takes the opposite route. The site should show how I structure software, data and cloud work: small components, explicit contracts, reproducible builds and clear guardrails.
That does not mean every personal site needs a heavy platform. Here the extra engineering has a purpose: the site has multilingual routes, case content, SEO metadata, theme behaviour, security headers, edge/origin protection and deployment infrastructure. Those are exactly the parts that tend to become messy in production apps when they are bolted on later.
The question was therefore not: "how do you make a nice homepage?", but: "how do you build a portfolio that demonstrates the same engineering discipline as the projects it describes?"
Architecture
The repository is shaped as a small server-rendered application:
Application
FastAPI routes → Jinja2 templates → locale-aware SSR pages
Content
cases.py + i18n.py → portfolio cards, case pages, chrome copy
Frontend
tokens.css → components/pages CSS → vanilla JS enhancements
Delivery
npm build:css → Docker image → GitHub Actions → Terraform → Cloud Run
FastAPI serves the routes, Jinja2 renders HTML on the server, and the content layer feeds templates with explicit Dutch and English records. Static assets are loaded with a version query and immutable cache headers. The Cloud Run infrastructure lives in Terraform; GitHub Actions runs quality gates before building and deploying the image.
Application layer
The route topology is centered in app/api/routers/pages.py. One router factory builds the Dutch canonical routes and the English /en/* routes, so handlers cannot drift by locale.
Deep-dive cases are driven by app/content/cases.py. When deep_dive=True, the template deliberately expects two includes: <slug>.nl.html and <slug>.en.html. There is no silent fallback to English; missing content should fail loudly.
SEO belongs to the same application layer. template_response() injects canonical and hreflang data, while meta.py serves robots.txt, sitemap.xml and /healthz.
There are no separate locale sites, no client-side language switch and no hand-maintained sitemap. Routes, cases and SEO metadata are derived from the same source data.
Frontend and design system
The frontend is intentionally built without a client framework. Jinja provides the HTML; JavaScript adds interaction where it earns its keep: theme switching, mobile navigation, the portfolio peek panel, terminal behaviour, reveal effects and case lightboxes.
app/static/css/src/tokens.css is the token layer. Dark mode follows the Retro Developer Terminal direction; light mode follows the Polar Blueprint direction. The two themes do not have to share the same aesthetic, but they do have to be driven through tokens and explicit light-mode selectors.
Case pages and architecture visuals use existing classes such as .case-main--cat-* and .case-archviz. That keeps accents, lightbox behaviour, reduced motion and light/dark overrides inside the existing CSS contract.
Content architecture
app/content/cases.py is the single source of truth for portfolio cards: slug, category, bento size, title, blurb, TL;DR, stack, metrics, deep-dive status and architecture flag. Locale fields are dictionaries with required nl and en values.
Short chrome copy lives in app/content/i18n.py. Long-form case copy lives in per-locale Jinja includes. Tests verify that deep-dive includes exist, section counts stay aligned and macro usage between Dutch and English cannot quietly diverge.
New cases therefore scale without route changes: add a data record, write two locale includes and optionally wire an SVG partial for the architecture view.
Deployment & operations
The Dockerfile uses a Node builder stage for CSS and image optimisation, followed by a Python runtime with uv. The runtime starts Uvicorn on port 8080, matching Cloud Run.
Terraform manages the Cloud Run service, service account, Cloudflare resources and monitoring configuration. The Cloud Run module sets health probes, min_instance_count=0, max_instance_count=1, request concurrency and environment variables such as APP_VERSION, CLOUD_REGION and CLOUDFLARE_ONLY.
GitHub Actions runs an infra plan, linting, format check, mypy and coverage tests before the deploy job builds, pushes and applies the Terraform plan. The post-deploy smoke test intentionally expects a 403 on the direct run.app URL, proving that origin bypass through CloudflareOnly stays fail-closed.
AI-agent guardrails
The repository contains explicit rules for AI-assisted development. AGENTS.md, DESIGN.md, CLAUDE.md, GEMINI.md and app/static/README.md define how agents must handle privacy, i18n, CSS tokens, theme splitting, frontend validation and audit output.
For theme and frontend work, a full trace is required: tokens → CSS consumers → templates → Python content → JavaScript runtime styles → SVG classes → routes → browser validation. That strictness is intentional, because small colour or specificity fixes can otherwise break dark mode, light mode or CSP.
The docs do not only describe intent. They encode concrete prohibitions and checks, including no direct environment reads in app/, nonce coverage for inline scripts/styles, no inline style attributes for CSP-sensitive elements and IaC-first infrastructure changes.
Engineering decisions that matter
- SSR over SPA. The site is content- and SEO-heavy; FastAPI/Jinja2 keeps runtime complexity lower than a client-heavy app.
- Vanilla JS where it is enough. Theme, navigation, terminal and case interactions do not need a framework.
- Tokens as contract. Colour and surface behaviour belong in CSS variables, not scattered through templates or JavaScript.
- Content separated from rendering. Portfolio metadata lives in Python data; long-form copy lives in locale includes.
- Asymmetric themes. Dark and light mode share components, but not blindly the same aesthetic.
- IaC and CI as baseline. The personal site uses the same reviewable build and deployment habits as production platforms.
Result
jaridata.dev mainly proves a way of working. The site shows that I do not only describe data/cloud/AI architecture, but also build my own public platform with the same discipline.
- server-rendered FastAPI/Jinja2 application
- Dutch and English routes with canonical and hreflang
- content-driven portfolio cases
- token-based dark/light design system
- CSP, security headers and Cloudflare-only origin protection
- immutable static caching with versioned query strings
- Docker and Cloud Run deployment path
- Terraform, GitHub Actions and testable guardrails
Status
The current site is live as a production-like portfolio app. The code includes routes, templates, i18n, SEO, middleware, tests, Docker, Terraform and CI/CD context. Some commercial features are intentionally not active yet: the contact page is a placeholder and functional data/AI API endpoints are listed in the README roadmap, not shipped in the current app.