How This Blog Is Built

I wanted a blog that was fast, simple, and entirely under my control. No CMS, no JavaScript frameworks, no database server. Just a single binary that starts in milliseconds and serves everything from memory.

Here's how it works.

The Stack

  • Go — the entire server, templating, and content pipeline
  • SQLite (in-memory) — querying posts, pages, tags, and series
  • Goldmark — Markdown to HTML rendering with syntax highlighting
  • templ — type-safe HTML templates compiled into Go code
  • Plain CSS with custom properties, dark mode via prefers-color-scheme
  • Deployed as a single Docker image on Coolify behind Traefik

Content as Embedded Markdown

Every post and page is a .md file with YAML frontmatter:

---
title: How This Blog Is Built
slug: how-this-blog-is-built
tags: [go, architecture]
status: published
published_at: 2026-04-06
---

These files live in src/content/posts/ and src/content/pages/. At compile time, Go's embed directive bakes them into the binary. There's no filesystem access at runtime — the binary contains everything.

In-Memory SQLite

When the server starts, it parses all the embedded Markdown files, renders them to HTML, and inserts everything into an in-memory SQLite database. This gives me the flexibility of SQL queries (list posts by tag, filter by series, order by date) without the operational burden of a persistent database.

The database is read-only after startup. There's nothing to back up, no migrations to manage in production, no data to lose. If I want to change a post, I edit the .md file and rebuild.

I'm using ncruces/go-sqlite3, which compiles SQLite to WebAssembly and runs it via a pure-Go Wasm runtime. No CGO, no C compiler, fully static binary.

Type-Safe Templates

The HTML is rendered using templ, which lets you write templates as Go-like components:

templ PostPage(site SiteData, post model.Post) {
    @Layout(site) {
        <article>
            <h1>{ post.Title }</h1>
            <div class="post-body">
                @templ.Raw(post.HTML)
            </div>
        </article>
    }
}

A code generator turns these into Go functions before compilation. If I misspell a field name, the compiler catches it — not a runtime error on a page nobody visits.

No JavaScript

The public-facing site ships zero JavaScript. The mobile navigation uses a pure CSS checkbox toggle. Dark mode is handled by a CSS media query. RSS and Atom feeds are generated server-side.

HTMX is vendored in the repo for future interactive features, but it's not loaded yet. When I need it, it's one <script> tag away.

Build-Time Validation

Duplicate slugs, missing titles, broken frontmatter — these are all caught at build time, not runtime. The build runs go test ./... which parses and validates all content before compiling. If something is wrong, the binary doesn't get built, and the Docker image doesn't get pushed.

Email Obfuscation

I built a small Markdown extension for email addresses. Writing <span class="email">s&#64;sksk.<span>app.</span>site</span> in Markdown produces HTML with a CSS-hidden decoy subdomain that defeats email scrapers while remaining fully accessible to real visitors and screen readers.

Development Workflow

For local development, make dev runs templ in watch mode alongside Air for Go hot reload. Edit a Markdown file, save, and the server rebuilds in about a second. Edit a .templ file and the pipeline is: templ regenerates the Go code, Air detects the change, rebuilds, and restarts.

For production, make docker-run builds a multi-stage Docker image (~20MB), validates all content, compiles the binary, and starts the container.

What I Like About This

The entire blog is two things: a collection of Markdown files and a Go program that serves them. There's no state to manage, no database to maintain, no CMS to update. The binary starts in under 100ms and handles requests in microseconds.

If the server dies, I restart it. If I lose the server entirely, I rebuild from the repo. The source of truth is always the code and the Markdown files.

It's the simplest architecture I could build that still feels like a proper application rather than a static site generator. And because it's a real server, adding dynamic features later (search, comments, analytics) is a matter of writing Go handlers — not bolting on external services.