hugo-editor: A Claude experiment
I write and publish this blog with Hugo, a static site builder that uses YAML for its posts. It’s also what we use to build the Grafana docs site, so I use it often. For the past few years I’ve been using vim to write posts. The final site content is copied onto a server after it’s built.
I have an SSH client on my phone, so I can theoretically write posts from anywhere. However, using a text editor over SSH on a phone leaves a lot to be desired. There are also iOS git clients like Working Copy, but they can’t run a script to verify my builds. Before publishing, I need a separate step, and it’s not part of my writing workflow.
So I decided to play with Claude to build a basic blog editor. After about an hour I had hugo-editor, which just hosts a basic web app that interacts with the file system on a server directly. It has a simple plain-text, styled <textinput>
and periodically saves the contents to the file system. It also keeps a hugo development server running at all times so I can preview the entries. There’s a configurable “publish” command, which I have set up to run a hugo build and then rsync the contents to my web server.
I skipped over some more complicated things like user authentication and management because I host the editor itself on my private Tailscale network, which makes it easy to bookmark the URL on trusted devices only. If someone wants to add user handling, contributions are welcome, but right now I’m very happy with the fact that there’s no database here, just a file system. If I wanted to add password protection, I would probably just use something like Caddy basic auth, which is what I use to protect the apps that I expose for myself over the public internet.
When I use Claude at work, I give it very limited freedom, and spend a lot of time in a tight loop making changes. For this, I decided to let it off the leash a bit. If you look at the code, you’ll find comments that go nowhere. For example, the Hugo development server is started, and there’s a comment // Set up cleanup on program exit
, followed by code that does nothing to set up cleanup.
Claude decided to put everything in the main file, and use Go HTML templates for HTML rendering, which are parsed at startup time. This means the app can’t be distributed as a single binary. When I tried to convince it to //go:embed
the templates, it struggled mightily. It also means the code is fairly ugly. It made config flags and then never used them to actually configure the application.
I would continue to be very wary of any products that proudly call themselves “vibe-coded” because of how many simple errors are in this very basic app, but I did get something genuinely useful in less than an hour.