Rebuilding Lloyd Logs as a Go Markdown Site
The site had acquired a second job
Changing a sentence on a small technical log should not require remembering which part belongs to Hugo, which part belongs to nginx and which part exists only because the old theme expected it. That was the moment the previous stack stopped feeling lightweight.
At first I thought I needed a better publishing system. I needed fewer moving parts. The replacement is a small Go server that reads Markdown, renders normal HTML and can be understood from the route to the content file without an archaeological dig.
Requirements
The requirements were deliberately HTML heavy and progressed with following mental model:
- Markdown should remain the source of truth.
- Posts should live under
content/posts/. - Pages should live under
content/pages/. - Drafts should stay hidden unless explicitly enabled.
- The site should run without a database, admin panel, React app, or heavy CMS.
- The Docker image should be self-contained in production.
- The layout should stay simple, readable, and easy to maintain.
Those constraints kept the design focused. The goal was to distill the sevices used to their core. To build the smallest durable system that made publishing easier and feel more native.
Architecture
The new site is a Go net/http application. It loads content from disk, parses frontmatter, renders Markdown, and serves full HTML pages.
The main pieces are:
net/httpfor routing and serving requests.html/templatefor server-rendered HTML.goldmarkfor Markdown rendering.- Plain files for content and static assets.
- Docker for deployment.
This is intentionally boring technology. The site does not need client-side routing or a build pipeline for JavaScript. The pages are normal HTML, so they work with JavaScript disabled.
Content model
Posts and pages are Markdown files with frontmatter:
---
title: "Example post"
date: 2026-07-09
tags: ["Go", "Markdown"]
draft: false
summary: "Short summary."
---
Slugs come from frontmatter when provided, otherwise from the filename. Published posts are sorted newest first. Draft posts are excluded unless the server is started with SHOW_DRAFTS=true.
Keeping content as files makes the workflow easy to reason about. There is no database migration to run, no admin account to secure, and no hidden state outside the repository.
Obsidian publishing workflow
I still wanted the writing experience to work with Obsidian. The local vault lives at:
/home/alex/Documents/lloydlogs
The production application does not depend on that path. Instead, a local sync script copies Markdown into the repository:
OBSIDIAN_CONTENT_DIR=/home/alex/Documents/lloydlogs go run ./cmd/sync-obsidian
That boundary matters. The local vault is an editing tool. The deployed app only reads from the content copied into the repo and packaged into the Docker image.
Docker deployment
The Docker build now compiles the Go binary and runs it directly. There is no Hugo build stage and no nginx runtime stage.
The production image reads content from:
/app/content
That keeps the deployment simple: the image contains the binary, templates, static files, and Markdown content needed to serve the site.
What I learned
The main lesson was that removing infrastructure can be just as valuable as adding it. Hugo and nginx were reasonable tools, but for this site they introduced extra concepts without much benefit.
This rebuild also reinforced a few habits I care about:
- Keep the production boundary clear.
- Prefer simple server-rendered HTML when it solves the problem.
- Make local workflows convenient without leaking local assumptions into deployment.
- Write documentation so future changes are easier.
What I would improve next
I would add a small preview command for draft content, improve test coverage around route behavior, and make the Obsidian sync workflow more explicit about page versus post mapping.
The local Obsidian vault remains an editing tool; go run ./cmd/sync-obsidian copies publishable Markdown into the repository, where the deployed image reads it. That boundary is slightly less magical than a live-sync CMS, but it makes the source of truth and release contents obvious.
I now value small systems partly for their refusal to make publishing feel like an integration project.