Update :: live demo now available

A demo of this concept is now available at https://garbagespeak.com and the demo source code is available at https://github.com/acaloiaro/garbagespeak.com/.

Intro

I’ve encountered a lot of skepticism around the idea of adding dynamic behavior to Hugo sites with hugo-htmx-go-template. That skepticism is well founded, because Hugo bills itself as a static site generator.

So why would anyone want to add dynamic functionality to Hugo sites when Hugo aspires to be a simple static site generator?

Because Hugo is a lot more than a static site generator. “Staticness” is simply the end result of a lot of great site building features coming together as a “built” site that doesn’t change. Staticness is one feature among many great Hugo features. I encourage you to visit https://gohugo.io/ if you’re not familiar with Hugo’s capabilities.

What does it even mean to add dynamic behavior to Hugo sites? I thought Hugo only output HTML and site assets

Adding dynamic behavior to static sites can be achieved in a number of ways. It can be as simple as dynamically loading new content onto the screen to give pages a more “interactive” feel, or it can be as complex as adding an entire comment system to blog posts. Using the techniques outlined in this post, there is no limit to how complex one’s Hugo site can become, though I recommend exercising restraint in making static sites dynamic.

Is it wrong to make non-static Hugo sites? Doesn’t that violate Hugo’s intended purpose?

First of all, it’s not wrong. Like most Hugo features, staticness is optional. There is no morality police dictating that Hugo sites are all static. Hugo is a great tool for building websites and if you have dynamic needs, entertain the approach outlined here. Don’t reach for a SaaS service because your Hugo site needs a contact form.

Examples

I’m a big proponent of explaining ideas through examples. Let’s start with the first example above: dynamically loading site content.

While this example may seem contrived and overly simple, I encourage you to ponder how the technique shown can be used to create more interactive websites.

If you’d like to follow along, clone the hugo-htmx-go-template starter project:

git clone git@github.com:acaloiaro/hugo-htmx-go-template.git

Dynamically loading content

As the name of the project suggests, I’ve opted to use htmx for interactive behavior. If you’re not familiar with htmx, it is what allows us to dynamically fetch content without writing a lick of Javascript.

We start by configuring Hugo to serve all the HTML files in a directory. Its configuration file is edited as follows:

open ./config/_default/config.toml

[module]
 [[module.mounts]]
 source = 'partials'
 target = 'content'

These configuration lines tell Hugo to take the directory partials and all of its contents, and make them available as content. That is, make the contents available to our site users.

This allows us to fetch any HTML file from partials with htmx (or any HTTP requests).

So for example, we can fetch the file partials/content.html by adding the following HTML to any markdown file. You can find the following snippet at the bottom of content/posts/hello-world.md.


{{< html.inline >}}
<div
  hx-get="/content.html"
  hx-trigger="click"
  hx-target="#content_target" />
  <button>Load content</button>
  <div id="content_target"></div>
</div>
{{< /html.inline >}}

This HTML div adorned with htmx attributes renders a button that when clicked, dynamically loads the contents of content.html into the #content_target div.

hugo-htmx-go-template fetching content dynamically

This is all good and well. We can dynamically load content from our Hugo site when users interact with it. But what about adding real application functionality to Hugo sites?

Introducing an API server

hugo-htmx-go-template comes with a bare-bones web server from which much more complex functionality can be built. It’s important to remember that all web applications are simply a combination of forms and buttons that do stuff when users interact with them. We’ve shown how to make a button load content dynamically, but what about forms? Can our Hugo site process form data?

Indeed it can. And that means we can build entire web applications with Hugo as the templating and build system, and something else as the “backend”.

hugo-htmx-go-template provides such a backend. The project’s server.go is a Go web server that renders html/template or templ templates, for those who prefer to write templates in pure Go. Of course, this is only a rudimentary web server. As the name suggests, hugo-htmx-go-template is simply a template from which to build projects. It is itself a simple Hugo project with some added tooling, and an optional web server, but we’ll get to that.

Let’s process form data from our Hugo site.

Again open contents/posts/hello-world.md and examine the section starting with ## Let’s add a form

Here we again see an inline Hugo shortcode that renders some HTML adorned with htmx attributes to make it dynamic.


{{< html.inline >}}
<!-- Make sure the api base URL is set -->
<form hx-post="{{ .Site.Params.apiBaseUrl }}/hello_world_form">
  <label>Name</label>
  <input type="text" name="name">
  <br/>
  <button>Submit</button>
</form>
{{< /html.inline >}}

We see a few new things here. First of all you may notice that this is a html/template, which allows the form to issue a POST request to {{ .Site.Params.apiBaseUrl }}. This is the URL where our web server is running.

apiBaseUrl is configured in config/_default/config.toml for development and config/production/config.toml for production. It should point to wherever you run your API server.

The second thing is this new endpoint /hello_world_form.

If we open up server.go, we’ll see the line:

mux.HandleFunc("/hello_world_form", helloWorldForm)

And helloWorldForm simply processes the form value and says hello to it using a templ template:

func helloWorldForm(w http.ResponseWriter, r *http.Request) {
	name := "World"
	if err := r.ParseForm(); err != nil {
		ise(err, w)
		return
	}

	name = r.FormValue("name")
	if err := partials.HelloWorldGreeting(name).Render(r.Context(), w); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

Let’s see it in action

hugo-htmx-go-template processing form data and responding to it

Deployment

So how does this all fit together, and how can this be done in production? For details on deployment, I’ll direct you to the project’s README on that topic, but I’d like to nudge the reader toward deploying these hybrid hugo-htmx-go-api projects as single fat binaries. You can continue deploying your Hugo sites as static HTML and assets as you always have and run the API server separately. However, I find fat binary deployment far more pleasant.

I mentioned some “tooling” earlier. If you ran through the getting started documentation you’ll have two tools available to you

  • bin/develop: This runs both hugo serve and go run server.go together to retain Hugo’s development experience. If the user installs the air utility, they will also get hot reloading of server.go when the file is saved.
  • bin/build: This builds your entire Hugo site and API server as a single fat binary. Yes, this is not a “static” site, but it is a Hugo site, with the added benefit of a dynamic API for building richer Hugo-based web applications.

The output from bin/build is your entire Hugo site embedded in a single binary. It can be deployed as a single file, cross-compiled to any system architecture and operating system supported by Go, and run anywhere that you can run applications.

I hope you can see that this is not crazy

While I hope you can see that this is not entirely crazy, I don’t blame you if you do. I’ve certainly found a use for this frakensite pattern, and I think you might as well.

It should also be said that while I’ve used go as the language to implement the api server, this pattern is in no way limited by language. Use whatever you want; everything will be fine.

Enjoy!