Sign up

Setting up HTMX and Templ for Go

Hana Mohan

Last updated on

Why you should ditch Next.js for HTMX for your website

Sites built with JavaScript frameworks like Next.js are often complicated and rely heavily on JavaScript, even for primitive tasks like submitting forms, state management, hydration, etc.

Modern sites built with React tend to use JavaScript as a holistic language for serving, rendering, and routing in a website. The problem with this approach is that if you have business logic in a different programming language like Go for example, you have to create more abstractions and APIs between your backend and frontend.

Why not Next.js?

Next.js is a React meta-framework, which means that you’re already starting with a fairly large bundle size because of the React and Next.js dependencies. Rect-dom is ~ already 42kB (minified and gzipped). Next.js also sends a huge chunk of JSON in script tags for hydration and pre-loading data; for a simple static website, this is an overkill.

HTMX

HTMX is a small (~14k minified and gzipped), dependency-free JavaScript library. It doesn't matter which language you use for your backend or business logic, which is great because we can use our existing business logic code in Go for our website.

You can use a Go templating language to generate HTML markup and use HTMX to make the site dynamic.

Templ and Go

Templ is a tool for building HTML with Go. It includes features like a templating language but can really do more than that—server-side rendering, static rendering, loops, variables, and other Go stuff.

You can create a Templ component by creating a .templ file.

package main

templ hello(name string) {
	<div>Hello, { name }</div>
}

Templ comes with a CLI that can be used to compile .templ files to Go files, the Go files act as templates and can be rendered using an HTTP server.

Setup

Setting up a Go Web Server

  1. Create a new Go project:
mkdir hello-world
cd hello-world
go mod init github.com/<username>/<repository>

  1. Let's use Go's standard net/http library to create a simple web server; first, create a main.go file:
// main.go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		io.WriteString(w, "Hello World! \n")
	})

	http.ListenAndServe(":3000", nil)
	fmt.Println("Listening on Port 3000 🚀")
}

You can run this program with the command

go run .

If you visit localhost:3000 in your browser, you will see "Hello World!"

Setting up Templ

  1. To install the templ CLI you can use the go install command
go install github.com/a-h/templ/cmd/templ@latest
  1. To Install Templ in your project run
go get github.com/a-h/templ
  1. Create a Templ file hello.templ in the same directory as main.go and add the following code
// hello.templ
package main

templ hello(name string) {
	<h1>Hello, { name }</h1>
}
  1. Generate Go code:
templ generate

The generate commands creates a hello_templ.go file. The hello_templ.go file will have a hello function that can be called with a name argument.

  1. Use Templ with the web server import github.com/a-h/templ and replace the existing handler function with a templ.Handler function.
// main.go
import (
	"fmt"
	"github.com/a-h/templ"
	"net/http"
)

func main() {
	component := hello("Templ!")
	http.Handle("/", templ.Handler(component))

	fmt.Println("Listening on port 3000 🚀")
	http.ListenAndServe(":3000", nil)
}
  1. Run the Go program again:
go run .

If you visit localhost:3000, you will see a heading that says, “Hello, Templ!”

Setting up HTMX

  1. We first start by creating a layout.templ file in the root directory:
// layout.templ
package main

templ layout(title string) {
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{ title }</title>
    </head>
    <body>
        { children... }
    </body>
    </html>
}

All of our Templ page components will be wrapped by the Layout component.

This is helpful because we would not have to type the HTML boilerplate code in every Templ file, and the layout file would serve as a single file where we can import HTMX. We are using the template composition syntax in Templ to be able to create a parent component.

We pass a title and use it like { title } this syntax allows us to pass variables in the components and render them in the markup.

  1. Let’s import HTMX from unpkg using the script tag, alternatively, you can also download a copy of the minified HTMX and bundle it or import it.
<!-- layout.templ -->
	<!-- ... -->
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Title</title>
				<!-- HTMX -->
				<script src="<https://unpkg.com/htmx.org@2.0.3>" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
    </head>
	<!-- ... -->

Setting up Tailwind

  1. We need to install and configure the tailwind compiler
npm install -D tailwindcss
npx tailwindcss init
  1. Configure tailwind for compiling the CSS from Templ files
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./*.templ"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Make sure to specify the path where your Templ files are located, in this case we are doing everything in the root directory.

  1. Create an input.css file in the root directory and add:
@tailwind base;
@tailwind components;
@tailwind utilities;

  1. Start the Tailwind CLI build process in a separate terminal:
yarn tailwindcss -i input.css -o static/css/tw.css --watch
  1. Add the compiled CSS file to your layout.templ
<!-- Layout.templ -->
	<!-- ... -->
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
				<link rel="stylesheet" href="/static/css/tw.css"/>
        <title>Title</title>
				<!-- HTMX -->
				<script src="../lib/htmx.min.js"></script>
    </head>
	<!-- ... -->

We now have Templ, HTMX, and tailwind setup ready to use.

  1. We need to serve the static tw.css file from our go server
package main

import (
	"fmt"
	"github.com/a-h/templ"
	"net/http"
)

func main() {
	component := hello("Templ!")
	http.Handle("/", templ.Handler(component))

	// server static files
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", fs)

	fmt.Println("Listening on :3000")
	http.ListenAndServe(":3000", nil)
}

Putting it Together

  1. Let’s use the layout component in the hello.templ file
package main

templ hello(name string) {
  @layout("👋 Hello World!") {
    <h1 class="text-xl text-violet-800">Hello, { name }</h1>
  }
}
  1. Compile the Templ file to Go and run the Go server
templ generate
go run .

Visit localhost:3000 to view the page! 🚀

Development Setup

The setup works but is quite tedious to use; it requires us to restart the Go server and re-run the templ generate command every time we make any changes in the codebase. We also need to refresh the browser every time to see the change.

Watch Mode for Go

There are a few options for watching file changes and re-running Go automatically like air and gow. Both of them are excellent tools and work for us. Let's use gow to watch for changes and restart Go server:

gow run . -e go,mod,html,css

Watch Mode for Templ

Templ's command line tool comes with a built-in watcher which recompiles the templ files to Go every time there is a change:

templ generate -watch --include-timestamp false --include-version false

But we can do better with Templ's proxy argument.

Live Reloading

We don't need to re-run go or templ anymore but we still need to refresh the browser every time there is a change. We can use the templ's command line tool's proxy for hot reloading the browser, let's add some more arguments to templ generate:

templ generate -watch -include-timestamp false -include-version false -proxy="<http://localhost:3000>" -proxyport="8080" -open-browser=false -cmd="gow run ."

This command will start a proxy server to run on the port 8080. You can visit localhost:8080 for development, and it will live reload every time a Templ or Go file changes.

Learnings from Setup

JavaScript's ecosystem to develop websites is more mature than Go and provides a lot more features out of the box like -

  • File-system based routing
  • Zero-config live reloading
  • Easier to deploy on services like Cloudflare and Vercel
  • Reliablility in setup.

Surprises on the way

  1. Poor MDX support: Our existing documentation uses MDX for the content, and we could not find any library that supports MDX out of the box.
  2. Go learning curve: As someone who has primarily built websites using JavaScript, learning Go can take some time.
  3. Lightning Fast Speed: Using Go makes the website faster to open and work with than holistic JavaScript frameworks.
  4. The setup is simple: After the initial setup, most things just work and are easy to debug. The generated HTML is easier to read and debug, and there is no framework-specific noise when debugging.

Next Steps

Using Go and HTMX is still new to us and presents different challenges that we may want to explore.

  1. Alpine.js: HTMX does not have client-side state management, refs, and other solutions baked in like other JavaScript frameworks; instead, it recommends scripting libraries like Alpine.js. For simpler static sites that don't require complex states a library like Alpine.js is often sufficient.
  2. Static Rendering: Templ has the ability to render static HTML files, which could help improve performance.
  3. MDX Support: MDX uses React components and JSX, which requires compilation, to use our existing components in the documentation.

Keep an eye out on our Twitter as we learn more about building static sites with Go.