A Book A Month: Hugo Edition

I've had my first try at moving semi-dynamic content from WordPress to Hugo.

At this point, I wouldn’t be surprised if you’re getting really tired of my posts about the site’s transition from WordPress to Hugo. But that’s mostly what I’m doing in my spare time these days. It probably sounds unbelievably boring, but the nerd in me is having a great time.

In addition to a lot of minor tweaks under the hood, I’ve now moved the A Book A Month feature from WordPress to Hugo. Since 2015, I’ve tried to read a book each month, and A Book A Month is my attempt to keep track of them.

Dynamic Living in a Static World

The main difference between everything I’ve moved to Hugo so far, and A Book A Month, is that the information being shown is dynamic. Well, at least semi-dynamic. Whenever I’ve had a reading session, I register for which book, the number of pages I’ve read, and a review score for the session in a simple web form.

On WordPress, displaying up-to-date information on each book was a simple matter. A few lines of PHP code pulled the most recent data from the database, and displayed it to the user. With Hugo, however, it’s a bit more complicated since every page on the site is generated when the it’s built. This means that the information displayed on individual book pages would be the data that was available at build time, and not necessarily the most recent data.

There are several possible ways to solve this.

One would be to use AJAX. Hugo would only serve an HTML page with placeholders, and a little JavaScript magic would fill the placeholders with data. I’m not a huge fan of this approach. It would require more code to be sent to the client, and the client would have to make another request to retrieve the book data from the server.

Another way to solve it is by using an iframe. An iframe is an HTML element that is used to embed another HTML document inside an HTML document. The most common use these days is to embed things like videos and advertisements. But I could use an iframe to embed the dynamically generated information on each book page. Personally, I’m not a big fan of this approach either. For embedding content from another site, like YouTube videos, using an iframe is an elegant solution since it provides an explicit separation of content from your site and the third part site. But for embedding content from your own site, the use of iframes in this way just feels clumsy.

What Did I End up With?

To move A Book A Month from WordPress to Hugo, I’ve used custom content types, data templates, and a REST API.

First off, I created a custom content type I called books, and a separate Markdown file for each book. The markdown files only contain a few line of front matter metadata, like the title of the book, the author, when I planned to read it, and the book’s ID in the database.

Next, I created two layout templates for the books content type, one for lists and one for single entries. The list template pulls the metadata from the front matter to display the A Book A Month main page. The single template makes a call to a REST API with the ID in the front matter of the Markdown file for each book. The API returns a JSON data structure that the template uses the display everything correctly.

To make sure that the A Book A Month section is updated whenever I update the data in the database, I added a call to Jenkins that triggers a build of the website. This is done every time I use the web form to update metadata on a book.

All this makes it possible to keep the site entirely static, and at the same time make sure it’s up-to-date when something changes in the database. Hooray!

It’s Not Perfect (Yet)

Is this the perfect solution? Well, it has some minor issues:

  • Instead of requiring that every books has its own Markdown file, the list template could probably also have been populated with data from a REST end point. But using separate Markdown files made it easy to use a lot of functionality that was already in place. Maybe I’ll change that one day, but right now, it’s not a big deal.
  • When the Jenkins build is triggered, the entire site is built. I suspect that it’s possible to tell Hugo to just build a particular content type, but I haven’t managed to figure out how just yet. Building the entire site isn’t a huge issue anyway. Everything, including publishing it to my server, takes about 90 seconds.
  • If the REST API that exposes the data about the books isn’t working, I suspect the entire build will break. This is not a major issue, since I’m at both ends of the problem - I’m both providing and consuming the API. But if I was using an external API to fetch the data, I would have created some fallback mechanisms in case it was unavailable.

Hugo <3

The more I work with Hugo, the more I like it. I often find myself in a situation where I want to do something with Hugo, and assume that it’s hard or even impossible. But after some searching in the documentation, forums, and even the registered issues on GitHub, I’m able to do what I want 9 out of 10 times.

The $2 I send Hugo’s lead developer bep every month is most certainly worth it.

If you’re looking for a static site generator, I can wholeheartedly recommend Hugo. I’ve only used it for my private site, which you’re looking at right now, but if I ever get in a situation where we have a challenge that can be solved with a static site generator at work, I have no doubt Hugo will be my recommendation and personal choice.


This post has no feedback yet.

Do you have any thoughts you want to share? A question, maybe? Or is something in this post just plainly wrong? Then please send an e-mail to vegard at vegard dot net with your input. You can also use any of the other points of contact listed on the About page.


It looks like you're using Google's Chrome browser, which records everything you do on the internet. Personally identifiable and sensitive information about you is then sold to the highest bidder, making you a part of surveillance capitalism.

The Contra Chrome comic explains why this is bad, and why you should use another browser.