Hi, I’m Max. This is my first post here, so it felt right to start with the thing you’re reading it on.
I mostly write to help myself think things through. I find that writing down my thoughts helps me understand them better, and keeping a public blog makes me more likely to finish my thoughts. Expect posts on whatever I’m working on or breaking at the time, usually technical, but not always.
People often think you need a CMS for a personal site. This one’s a folder of Markdown files, a static site generator and a CI pipeline. There’s no database to patch, no admin panel to get breached and nothing to keep running between deploys. Here’s the full story.
The generator: Hugo
Everything lives under content/ as plain .md files. Hugo (extended edition) turns them into static HTML at build time. A new post is just one command and some Markdown:
hugo new blog/my-post.md --kind blog # then write below the front matter
Front matter drives the rest: title, date, tags (the first tag becomes the category label), and description (the list teaser, search snippet and meta description, all from one field). Hugo generates the tag pages, RSS, pagination and search index from the content itself, so I never have to hand-edit them.
Hugo makes this almost too easy. It’s a quick binary, and there’s no toolchain to maintain, which is great. It also stays out of the way while I’m writing.
The theme is the point
The look is a custom theme, not an off-the-shelf one: warm paper #f9f5ec, a single terracotta #c1522a accent, Sora for text and JetBrains Mono for code, and a // code-comment logo. There’s one light theme and no dark-mode toggle, and the only animation is a subtle scroll reveal.
Building it was the most fun part so far. A theme from scratch means every small decision is mine, and playing with color and type never really felt like work.
Hugo Pipes does the asset work
No Webpack, no bundler. It all happens at build time:
- CSS and JS are fingerprinted (hashed) for cache-busting.
- Fonts are self-hosted
.woff2(variable, latin subset), so no Google Fonts request leaves the page. - Pictures are resized to responsive WebP at build time.
That’s why the site needs the extended edition: image processing and font handling depend on it.
The dynamic bits, without a backend
Four things that might look like they need a backend but actually don’t:
- Press
/on the blog page to search. It runs fully client-side, fed by an/index.jsonfile Hugo emits from the content. - Syntax highlighting is Hugo’s built-in Chroma, which is rendered to CSS classes (that dark terminal-card look), so there’s no highlight.js at runtime.
- The contact form currently falls back to a prefilled
mailto:. (#TODO) - Analytics is self-hosted, cookieless Umami and it only loads in production builds, never while I’m previewing locally.
Deploy is a git push
Just push a branch and GitLab CI will build it and put up a preview. Merge to main and the same pipeline ships to production. Minus the plumbing:
build:
script:
- hugo --gc --minify --environment production -d .vercel/output/static
artifacts:
paths: [.vercel/output]
rules:
- if: '$CI_COMMIT_BRANCH' # every branch builds
deploy: # production
script:
- vercel deploy --prebuilt --prod --token "$VERCEL_TOKEN" --yes
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
preview: # everything else
script:
- vercel deploy --prebuilt --token "$VERCEL_TOKEN" --yes
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
The last two rules: conditions are the whole branching strategy. --prebuilt is the one I’m really interested in. Hugo runs on the GitLab runner, so Vercel only ever gets the compiled output in .vercel/output, never the repository. It’s the --environment production setting that activates the Umami snippet, which is why my local previews aren’t included in the analytics.
For now, that’s on Vercel, but since it’s only static files, I could move it somewhere self-hosted later without too much hassle. The source stays private either way and there’s no server-side runtime to keep patched.
Why bother
The great thing is that the whole site is a git repository. The content, layout, styling and deployment configuration are all versioned together and if you need to roll back, just use a git revert. There’s no CMS to upgrade and no attack surface between deploys.
That’s why the boring stack matters to me. I want somewhere to think out loud that’s still here after this year’s framework is forgotten, and a folder of text files plus a build step should manage ten years of that with little to no maintenance at all.