ProjectsBy Alison Alva••4 min read
Learning front end on the infra stack!
I mostly live in Devops/Infra, Embedded, and Security - front end is not my specialty but this website seems alright? I already built so much of the infrastructure, I figured I might as well learn front end to throw on top of it.
Front EndNextJSTypeScriptInfrastructureKubernetes
<p></p><h2 id="why-build-from-scratch">Why Build From Scratch?</h2><p>I could have used a template. Squarespace, a Gatsby theme, or one of the countless "developer portfolio" starters on GitHub. They're polished, they work, and they'd take a day or so. But where's the fun in that? </p><p>A portfolio for an infrastructure engineer that runs on someone else's infrastructure felt contradictory, and I suppose I could have used templates that I hosted locally but I figured I'm already here, might as well become the REAL full stack engineer from Circuits -> Front end and everything in between... for... fun? I don't know, I'm a nerd, it's what I do. </p><p>When building on my personal infrastructure, I have a few requirements</p><ol><li><strong>Content I own</strong> - Markdown files in git</li><li><strong>Infrastructure I control</strong> - Running on my own cluster, deployed through my own CI/CD</li><li><strong>FOSS! </strong>The entire point of running all this is I know I can always keep it up, I'm not a FOSS evangalist, but for this particular use case not using FOSS felt like a violation of my original intent.</li></ol><h2 id="blog-taxonomy-vs-project-showcase">Blog Taxonomy vs. Project Showcase</h2><p>The site has two organizational systems that serve different purposes.</p><p><strong>Blog domains</strong> categorize writeups by technical discipline:</p><ul><li>Firmware, DevOps, Security, Full-Stack, Projects</li></ul><p><strong>The hire page</strong> showcases specific projects I've built:</p><ul><li>Portfolio Site, Home Infrastructure, Monitoring Stack, GitLab CE, AntStack</li></ul><p>These aren't the same thing. Blog posts document how I think through problems. The hire page shows what I've shipped.</p><p>Here's how they connect: the "Home Infrastructure" project on the hire page highlights technologies like Kubernetes, Flux, and cert-manager. Clicking any of those shows blog posts tagged with them. More annoying than you would think to implement in a way that feels clean.</p><h2 id="typed-content-schema">Typed Content Schema</h2><p>Each post has YAML frontmatter validated at build time:</p><pre><code class="language-typescript">type TechnicalDomain = 'Firmware' | 'DevOps' | 'Security' | 'Full-Stack' | 'Projects'
interface BlogPost {
id: string
slug: string
title: string
excerpt: string
content: string
date: string // ISO 8601
author: string
tags: string[]
domain: TechnicalDomain
readTime: number
featured: boolean
}
</code></pre><p>The frontmatter is parsed with <code>gray-matter</code> and passed through a runtime validator that checks it against this type. If I misspell a domain (<code>'DevOPs'</code> instead of <code>'DevOps'</code>), the build fails instead of silently breaking filters. This caught a post early on that had <code>featured: 'yes'</code> instead of <code>true</code> - it wouldn't appear on the homepage until the type check caught it.</p><p>Pages are statically generated at build time - no database queries, no cold starts for content. The container runs Next.js in standalone mode, which bundles everything needed into a minimal Node.js server. Simpler than managing separate build and serve containers.</p><h2 id="the-stack-diagram">The Stack Diagram</h2><p>The hire page centers on an interactive visualization instead of a skills list.</p><h3 id="why-not-a-list">Why Not a List</h3><p>Every developer portfolio has a "Skills" section. Most look like this:</p><blockquote><strong>Languages:</strong> Python, TypeScript, Go, Rust<br><strong>Tools:</strong> Docker, Kubernetes, Terraform</blockquote><p>These lists are impossible to differentiate. Everyone claims the same skills. There's no context for depth or how pieces connect.</p><p>The stack diagram shows something different:</p><pre><code>┌─────────────────────────────────────────────────────────────┐
│ [ Portfolio Site ] [ Home Infra ] [ Monitoring ] ... │ ← Projects
├─────────────────────────────────────────────────────────────┤
│ Frameworks: Next.js │ React │ Flask │ Grafana │
│ CI/CD: GitLab CI │ Docker │ Flux │ ← Tech layers
│ Runtime: Kubernetes │ Ingress │ Cloudflare │
│ Languages: TypeScript │ Python │ C++ │ Bash │
│ Infrastructure: Bare Metal │ MCUs │ Custom PCBs │
└─────────────────────────────────────────────────────────────┘
</code></pre><p>Clicking a project highlights the technologies used to build it. Clicking a technology shows blog posts that demonstrate competence with it. I have unfortunately created an entire secondary workload for myself with this, though, finally going back and writing blog posts for the various things I've made over the years. A necessary evil, I suppose, I'm just glad I kept notes. </p><h3 id="design-decisions">Design Decisions</h3><p>Early on I considered SVG connection lines between projects and technologies - pipes that would light up to show relationships. I scrapped it. The highlight state <em>is</em> the connection visually. Simpler implementation, fewer bugs, easier to maintain.</p><p>The layout uses standard flexbox rather than custom positioning. This means it works with web layout instead of fighting it - mobile-friendly by default, semantic HTML, no fighting CSS.</p><p>I also deliberately left things out. I oonly include technologies I actively use and can discuss in depth. Lists padded with "touched it once" skills waste everyone's time.</p><h3 id="data-driven-rendering">Data-Driven Rendering</h3><p>The diagram isn't hard-coded. Each project is a data object that lists which tech boxes to highlight and which tags to filter posts by. The component reads that config and renders accordingly, so adding a new project is "add one object to the config," not "hand-edit a bunch of JSX."</p><p>The diagram auto-cycles through projects to give visitors a quick tour, pausing on any click and resuming after inactivity.</p><h2 id="tech-choices">Tech Choices</h2><p><strong>Next.js 14 with App Router</strong> was a natural choice for a content-heavy site. Server Components reduce client JavaScript, static generation pre-renders everything at build time, and file-based routing keeps the structure obvious. I considered Astro for its content-first model but stuck with Next.js because I'm more productive in it.</p><p><strong>TypeScript everywhere</strong>, including content. The <code>TechnicalDomain</code> union isn't just documentation - it's enforcement. Typos in metadata become compile errors.</p><p><strong>Tailwind CSS</strong> with a constrained palette: slate-950/900/800 for backgrounds, white and slate-400 for text, blue-500/600 for accents. The utility-first approach keeps styling co-located with components rather than scattered across CSS files.</p><h2 id="deployment">Deployment</h2><h3 id="the-pipeline">The Pipeline</h3><p>The site runs on a personal Kubernetes cluster. The deployment flow:</p><ol><li>Push to <code>dev</code> → GitLab CI builds container → deploys with <code>:dev</code> tag</li><li>Merge to <code>main</code> → builds container → deploys with <code>:latest</code> tag</li><li>Flux watches the GitOps repo and applies updated manifests when the image tag changes</li><li>Rolling update with 2 replicas → zero-downtime cutover</li></ol><p>Health checks matter here. The pod has a readiness probe on <code>/api/health</code> - Kubernetes won't route traffic until it passes. If a deploy is broken (missing env var, bad config), the new pod never becomes ready and the old one keeps serving.</p><p>For TLS, cert-manager handles Let's Encrypt certificates automatically. The <a href="https://ghost.scopecreep.productions/blog/automated-tls-certificates-kubernetes">TLS certificates post</a> documents the setup and a debugging session when certificate DNS names weren't matching.</p><h3 id="the-tradeoff">The Tradeoff</h3><p>Self-hosting means real operational burden. When the cluster has issues, it's my problem. When certificates expire unexpectedly, I debug it. </p><p>If it weren't for my original design intent of "I own the entire stack" I'd probably offload TLS termination and CDN to Cloudflare, host the app on Vercel or Fly.io, and save the self-hosted infrastructure for internal services where I actually need control. The portfolio being on my own cluster is educational; a production app needs boring.</p><p>I'm also planning a static mirror of the key pages (hire + a few posts) on a managed host so that if my homelab is down, the entry point recruiters see still works. That'll eventually get its own post when I have the time. </p><h2 id=""></h2><h2 id="the-stack">The Stack</h2>
<!--kg-card-begin: html-->
<table>
<thead>
<tr>
<th>Component</th>
<th>Choice</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Framework</strong></td>
<td>Next.js 14</td>
<td>Modern React patterns, SSG</td>
</tr>
<tr>
<td><strong>Language</strong></td>
<td>TypeScript</td>
<td>Type safety applied to content</td>
</tr>
<tr>
<td><strong>Styling</strong></td>
<td>Tailwind CSS</td>
<td>Systematic design constraints</td>
</tr>
<tr>
<td><strong>Content</strong></td>
<td>MDX + gray-matter</td>
<td>Content-as-code workflow</td>
</tr>
<tr>
<td><strong>Container</strong></td>
<td>Docker (multi-stage)</td>
<td>Production-ready builds</td>
</tr>
<tr>
<td><strong>Orchestration</strong></td>
<td>Kubernetes</td>
<td>Operating shared personal infra</td>
</tr>
<tr>
<td><strong>CI/CD</strong></td>
<td>GitLab CI CE</td>
<td>Full DevOps pipeline ownership</td>
</tr>
<tr>
<td><strong>Pipeline Workers</strong></td>
<td>Gitlab Workers running on K8S</td>
<td>Github broke my trust trying charge self-hosted</td>
</tr>
<tr>
<td><strong>TLS</strong></td>
<td>cert-manager + Let's Encrypt</td>
<td>Automated certificate lifecycle</td>
</tr>
</tbody>
</table>
<!--kg-card-end: html-->