I built a simple blog / digital garden to host some writings and experiments in Go with minimal dependencies (this website). Main goals: fast loading, fast publishing, full control, host for free.

For the technology I wanted something simple and robust that I can setup (and learn or build) in a weekend. The blog should be a minimalist static site, that loads lightning fast, has no frontend bloat and I can host for free. The author/maintainer experience should be developer-friendly. It could be as simple as writing content as HTML, but content and theme should be decoupled so one can change without the other. Ideally I want to write the content in Markdown, so naturally I looked at the popular Hugo framework. People seem to either love or hate Hugo, it seems to have some initial learning curve. (Another intriguing framework is Astro, which I might choose for more interactive / content-driven websites with a more elaborate design, React integration etc.)

I’ve also been planning to do more fun side projects and to gain more experience with Go for a while, so I decided against Hugo and to build my own simple static site generator. This is not the most original “Learn Go” project but it gave me something useful to work on and the most control over the final site.

Stack

  • Site generator:
  • Theme:
    • custom HTML templates & CSS, a little vanilla JS sprinkled in
  • Hosting:
    • (TODO) Hosted on Cloudflare Pages
    • built and deployed by Github Action

Features

Built basic blog features: posts, posts on homepage, a few custom pages, tags, RSS feed (TODO)

DevEx

  • Rely on Go’s static type system to enforce data consistency1
  • Theme is decoupled from content and logic
  • (TODO) Publishing a new post is a simple git push to main, CI/CD takes care of the rest
  • (TODO) Pushing to another branch generates a shareable preview URL of the website

Design

Some opinionated decisions / details I paid attention to:

  • With JS disabled, nothing important breaks
  • Blog post relative links are slugs that don’t change if the title/date changes
  • Links opening a new tab are marked as such (with a icon)
  • I love animations but they can make a website feel sluggish – hence a default --transition-duration: 0.07s :D
  • Typography matters – I used the goldmark typographer extension to ensure proper rendering of “quotes”, n-dashes (–)2 and ellipsis (…). Ligatures (fi, ffl) are supported in modern browsers by default if the font has them.
  • Some inspiration from Tufte CSS: a serif font for the main prose, contrast between text and background not too harsh, links are uncolored and underlined. (And I want to do side notes in the future …)
  • Don’t waste vertical space: I feel a lot of modern web design overdoes it with big beautiful typeface, I played around with the font-size to find one that feels nice. On small screen sizes we scale down to :root { font-size: 90%; }. I’m torn about the sticky navbar (40px).
  • Spacing carefully optimized by hand: headings & paragraphs, headings following headings, nested list items, code listings in lists, etc.
  • Headings get linkable anchors, made possible by goldmark/anchor

Methodology

I split the project into two phases, scoping/research and executing. Friday night after work I sat down to gather the requirements, get a rough idea of the design (pretty much a clone of any minimalist SSG blog) and research the tech stack. Overall I’m pretty happy with this split in two phases, this let me focus on the execution without going down into too many rabbitholes.

LLM support

For the scoping/research phase I used Gemini (2.5 Flash) as a sparring partner. However I found that as a product it’s not quite there yet, e.g. its code output opens in as Google-doc like view that doesn’t let you copy the raw code, some files are just never displayed, and it cannot access the internet to search sources / verify its claims (although it tries very hard to gaslight you into believing that it can) – so after a while I was annoyed and switched to Claude.

I had some past Go experience but it was a bit rusty (no pun intended). For the learning experience I wanted to be very intentional about using LLM coding assistance and not have the whole project “vibe-coded”, however I did not want to struggle through every small syntax issue like it’s 2020. Also I’m not doing much frontend lately so I did not care to hand-craft CSS classes (that didn’t quite work out). (Why not tailwind: never been a fan, also I wanted to keep the styling code as minimal as possible.)

Since I mainly wanted to exercise Go, I asked Gemini some broad questions about project structure, development environment setup, best practices etc. which I verified online. (That is my current main LLM use case: treating them like a fuzzy search engine either if you don’t quite know the right terminology to search for, or have specific questions and are too lazy to navigate through documentation).

During the coding phase I used Claude Code with the initial prompt:

We are working on a little Go project for a blog static site generator. In the readme, you’ll see an overview of completed and to do features. One of my goals is to learn/practice Go syntax. So act as a mentor, don’t generate / edit Go files unless asked otherwise. But feel free to review/discuss/suggest changes, in a suggestive way that nudges me to figure out the details myself, unless I ask for details.

I don’t think this is the pinnacle of LLM-assisted learning but I feel it’s still essential to do the hard part yourself to master your tools and not be completely dependent on LLMs writing your code. The research phase also turned out some resources I found interesting and want to look into deeper.3

Some Go boilerplate code was indeed generated but I typed it out myself to practice the muscle memory (if err != nil ...). I disabled any Copilot / autocomplete to prevent cheating.

What I find a much bigger gain than code generation is having fast feedback cycles (at least to verify quickly if the slop code works): I am delighted that Go + VS Code pretty much work out of the box, so getting started was super fast, and rebuilding the application (plus the website) takes only one keyboard shortcut. (I do plan to make a watch command to get rid of even that.)

Implementation

Parsing Markdown

(TODO)

Template Engine

(TODO) Template engine: considered templ, gomponents

Conclusion

Things to improve

  • Handling of media/images – it works by placing them in the /static folder, but it would be nice to 1. have an automatic pre-processing pipeline (resizing, compressing, stripping metadata etc.), 2. serve different precomputed resolutions through a CDN, 3. open full-size overlays on click, 4. have relative links work out of the box with VS Code’s markdown preview
  • Limitations of goldmark: I reached the limits of the existing extensions with features like: open all links to external websites in a new tab, more custom syntax highlighting of code listings.
  • The table of contents on post pages floats on the left side (on big enough screens). I managed the stylesheet, responsivity, and parsing the content from the markdown using goldmark-toc and some custom logic, but it’s a bit wonky. I plan the same for footnotes as side notes attached to the text on the right side.
  • Provision infrastructure through IaaC

Random Learnings

  • How Go defines mnemonic constants for time parsing & formatting:
    t, err = time.Parse("2006-01-02", isoDate)
    
    (see https://www.pauladamsmith.com/blog/2011/05/go_time.html)
  • In CSS, width: 100vw assumes the viewport width without the scrollbar, it seems to be impossible to find out the width of the scrollbar and account for it with pure CSS (not counting some JS hacks). (I tried to achieve some cool conditional full page width effect for figures.)
  • Smooth scrolling to anchors can be achieved with CSS only:
    html { scroll-behavior: smooth; }
    
    (see https://gomakethings.com/smooth-scrolling-links-with-only-css).

Stats

Build time (tool), build time (website), bundle size (TODO)

Thoughts

Is it a minimal codebase?

The tool without the markdown content has < 1500 LoC (and almost half of that is CSS). The only relevant third-party dependency is goldmark.

Go vs. Python?

Honestly I was pleasantly surprised by Go’s development experience and simplicity. Python is probably going to stay my default choice for small / fun projects like this, but why not choose Go for some more production-grade scripts/tooling. I find the lack of “utility” methods such as reversing arrays somewhat annoying, it feels like every project is going to end up with a big util package (good thing we have Generics support now). No strong opinion on the error handling design (yet).

Was it worth building this?

Hopefully.


P.S.: No LLM was involved in writing this post, any errors are my own.


  1. I considered using Go structs for the frontmatter, or even using some more expressive config language like CUE, but in the end went with good old yaml ↩︎

  2. According to Merriam Webster, m-dashes (—) would be more correct in English—without space—but I prefer the look of n-dashes and I feel they are more commonly used in Europe. ↩︎

  3. Go resources I found interesting: https://roadmap.sh/golang, https://go.dev/doc/effective_go, https://100go.co/, https://lets-go-further.alexedwards.net/, https://quii.gitbook.io/learn-go-with-tests, https://awesome-go.com/, https://mtardy.com/posts/memory-golang/, https://go.dev/ref/mem, https://boldlygo.tech/media/ ↩︎