The site itself. A statically generated Next.js frontend where most pages ship as pure HTML with no client JavaScript unless interactivity is required. A FastAPI backend on the home server handles the dynamic features: contact form, activity feed, health monitoring, and CV downloads.
Static generation handles content; only a handful of components need client-side JavaScript. The build validates project data and checks repository URLs are reachable before generating pages, so broken links get caught before they reach production.
All project content lives in a single JSON file. No CMS, no database for content. Edit the JSON, push, and the build generates pages. Static generation via generateStaticParams creates a page for every project slug.
Build-time validation (validate-projects.ts) sends a HEAD request to every project's repository URL. If any link is dead, the build fails. This catches URL rot before it reaches production, not after a recruiter clicks a broken link.
Anti-spam without CAPTCHA. Three layers: a honeypot field (hidden input; if filled, the server returns fake success to fool bots without revealing detection), a time-based check (rejects submissions faster than 3 seconds after page load, also with fake success), and rate limiting (5 per hour per IP via slowapi). Every anti-spam response returns a 200, so bots never learn what tripped them.
On valid submission: store in SQLite, send email via ProtonMail SMTP (async, non-blocking), and send a push notification via self-hosted Ntfy. No CAPTCHA friction. Real users never notice the anti-spam; bots get a success response and think they got through.
Pulls recent commits from the Gitea API across the 10 most recently updated repos. Computes stats: commits this week, active repos, most active repo, current streak (consecutive days with commits). Private repo URLs are stripped from the response; metadata is preserved.
5-minute cache, with stale fallback up to an hour if the Gitea API is unreachable. The frontend transforms snake_case Python responses to camelCase TypeScript via a client library. Type-safe API layer with Pydantic on the backend and TypeScript interfaces on the frontend.
Path-filtered Gitea Actions. Frontend changes (anything outside backend/ and deploy/) build static HTML and push to Cloudflare Pages via Wrangler. Backend changes (backend/ or deploy/) SSH into the home server and rebuild the Docker container. Both pipelines trigger on push to main; path filters mean only the relevant service rebuilds.
The self-hosted runner and deploy target being the same machine keeps it simple: no remote SSH from a cloud CI service, no extra credentials beyond a Cloudflare API token for the frontend pipeline.
The frontend is a statically generated Next.js app deployed to Cloudflare Pages. Dynamic features (contact form, activity feed, health checks, CV downloads) are served by a FastAPI backend running in Docker on a home server, connected via Cloudflare Tunnel. Gitea Actions handles CI/CD with path-filtered workflows for independent frontend and backend deployments.
Static generation for content pages, client components only where needed for interactivity
Self-hosted CI/CD with path-filtered Gitea Actions workflows
Live health monitoring dashboard for all backend services
Contact form with ntfy push notifications
Activity feed aggregating recent work across projects
Dark and light theme with system preference detection
Static generation handles most content - only a few pages actually need client-side JavaScript
Path-filtered CI/CD in a monorepo keeps frontend and backend deploys independent
Self-hosted runners are straightforward when the CI server and deploy target are the same machine