A drunken panda falls into a bowl full of sake
If this title does not make any sense to you at all that is fine. At the end of this post it will. Mostly. Maybe. Last week I wrote about rebuilding my blog. Looking back at nearly 27 years of blogging I have pretty strict opinions on the software I am using. Not having the first 18 years of archives anymore is one of the reasons I am pretty insistent on having data available, backed up and readable. (Technically I could extract the old posts, but as they are all written in German so it would be a pain to bring them back. Besides that, if you think I sometimes have stupid ideas these days you would be in for a shock what they looked like nearly three decades ago.)
When I restarted my blog I wrote a static site generator that did serve me well for many years. I named it "drupan", also known as drunken panda. Guess which World of Warcraft expansion was just announced with an amazing trailer? My plan back then was to add a proper editing interface because I could never stand writing plain markdown files, no matter how convenient they are. But I did not get to it and other things took priority, so I settled on a simpler way and first used Wordpress and then a Django based CMS. The idea was to call the CMS "sakebowl". Got to stay on brand with badly named projects, right?
But what better time to pick up a project I wanted to get to 15 years ago than now? So I sat down and made a plan. Plans are good, right? That is how software is built. Ask any PM not worth their salary.
The backend will basically be the Django app I am using for this blog. It serves me well and hast all features I need. Direct file uploads to my web server would be nice so I do not have to open Transmit every time I want to include an image, but that is a pretty small problem to solve. The actual page should be served by a small app that reads a static blog, pre-generates all the pages and maintains a search index. So nothing too complicated. I will get a bit fancy in some corners as I plan to rewrite the Go service in Rust as a learning opportunity when I am done.

I added Django Rest Framework and a management command to export the whole blog to a blog.json file. There is not too much to it. I want the data to live on the web server itself, in case the node reboots and needs to rebuild the blog. I might use Paramiko and avoid rsync for simplicity.
Building the app in Go should be relatively simple. I removed the fields of the structures to make the code easier to read.
type post struct {
}
type tag struct {
}
type redirect struct {
}
type sakebowl struct {
Posts []post
Tags []tag
Redirects []redirect
}
func newSakebowl(path string) (sakebowl, error) {
data, err := os.ReadFile(path)
if err != nil {
return sakebowl{}, err
}
var sake sakebowl
err = json.Unmarshal(data, &sake)
return sake, err
}
The sakebowl structure holds all data read from the blob file. This would also allow me to add a second loader from an object storage for example. Not sure this is worth doing, so for now the filesystem reader is hardcoded.
With that I can build a cache that holds all the rendered pages for each URI.
type cache struct {
content atomic.Pointer[map[string]string]
}
func (c *cache) cacheBowl(sake sakebowl) error {
data := make(map[string]string)
for _, p := range sake.Posts {
// TODO: render
data[p.Slug] = p.Rendered
}
// TODO: generate index
// TODO: generate feed
// TODO: generate archive
// TODO: generate tag page
c.content.Store(&data)
return nil
}
func (c *cache) get(uri string) (string, bool) {
m := c.content.Load()
if m == nil {
return "", false
}
val, ok := (*m)[uri]
return val, ok
}
func newCache(sake sakebowl) (*cache, error) {
c := cache{}
err := c.cacheBowl(sake)
return &c, err
}
Usually I would opt for a sync.RWMutex. But why not use an atomic.Pointer instead, that might make my life a bit harder when rewriting the whole thing in Rust — and lead me down a rabbit hole of exploring thread safety and concurrent data access? It is also not that inconvenient. Sure, I got to dereference the map. I could likely force a race condition writing to the map if I wanted to. But otherwise it is pretty neat that the map stays around for ongoing requests and is replaced for new ones and not having lock contention for reads. I am sure one day my blog will be big enough for AI scrapers to hammer it for this to matter!
Also fun fact to keep in mind:
val, ok := (*m)[uri]
return val, ok
This is fine. This however is not:
return (*m)[uri]
If you want to learn more about Go than you likely ever need I would encourage you to track down why one of the two leads to a compiler error.
Progress
The blog is coming along nicely. I will run the current templates through a HTML to gomponents converter, mostly because I am lazy and do not plan on redesigning the blog right now. What is more interesting is the search functionality. A rudimentary implementation of BM25 is not too involved, but I want to spend a few minutes on figuring out if there is a better way of doing it.
posted on Aug. 16, 2026, 8:41 p.m. in golang, lazerbunny