Setting up Go TEMPL + Tailwind + HTMX

Setting up Go TEMPL + Tailwind + HTMX

The goal of this article is to set up a project template with the following features:

  • Set up TEMPL

  • Set up Tailwind CSS

  • Set up HTMX

  • Automatically rebuild CSS (Tailwind)

  • Live reload Go application on change using Air

This article assumes that you have Go, TEMPL, npm, and Air installed in your PATH.

Our project structure will look like this at the end of this article:

.
├── go.mod
├── go.sum
├── main.go
├── package.json
├── package-lock.json
├── static
│   └── css
│       └── tailwind.css
├── tailwind.config.js
└── view
    ├── index.templ
    ├── index_templ.go
    ├── layout
    │   ├── base.templ
    │   └── base_templ.go
    └── partial
        ├── foo.templ
        └── foo_templ.go

Setup TEMPL

go get -u github.com/a-h/templ
// file: view/index.templ
package view

templ Index() {
    <header>
        <h1 class="text-center text-3xl font-bold">
            Hello, World
        </h1>
    </header>
    <div class="flex justify-center mt-10">
        <!-- HTMX here -->
        <button hx-get="/foo" class="btn bg-teal-200 p-4 rounded-lg">
            FOO
        </button>
    </div>
}

As you can see, in the index.templ file, we have used Tailwind classes and HTMX's hx-get attribute. When the user clicks on the button, FOO should be replaced by BAR. The foo template is as follows,

// file: view/partial/foo.templ
package partial

templ Foo() {
    <h2 class="text-red-500 font-bold text-xl">
        BAR
    </h2>
}

Now, let's create a base layout that wraps the index page.

// file: view/layout/base.templ
package layout

templ Base(children ...templ.Component) {
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <title>Hello, TEMPL</title>
            <!-- tailwind css -->
            <link href="/static/css/tailwind.css" rel="stylesheet"/>
        </head>
        <body>
            for _, child := range children {
                @child
            }
            <!-- htmx -->
            <script src="https://unpkg.com/htmx.org@1.9.10"></script>
        </body>
    </html>
}

The base template takes in a variadic argument of children components that can be rendered inside it. It also includes the built tailwind stylesheet (we'll come back to this later) as well as imports HTMX.

Now that all our templates are in place, let's generate the corresponding go file:

templ generate

Finally, let's stitch everything together.

// file: main.go
package main

import (
    "log"
    "net/http"

    "github.com/murtaza-u/mytempl/view"
    "github.com/murtaza-u/mytempl/view/layout"
    "github.com/murtaza-u/mytempl/view/partial"

    "github.com/a-h/templ"
)

func main() {
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    c := layout.Base(view.Index())
    http.Handle("/", templ.Handler(c))

    http.Handle("/foo", templ.Handler(partial.Foo()))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

In addition to the template routes, we also need to create a file server to serve the Tailwind CSS stylesheet and other static assets (in the future).

Setup Tailwind CSS

In order to set up Tailwind, we need to initialize a node module:

npm init -y

Next, initialize Tailwind CSS,

npm install -D tailwindcss
npx tailwindcss init

Edit the generated tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./view/**/*.templ"], // this is where our templates are located
  theme: {
    extend: {},
  },
  plugins: [],
}

The content field is very important. It asks Tailwind to watch over all the files ending with .templ inside the view directory.

Next, add two convenience scripts to package.json:

"scripts": {
  "build": "tailwindcss build -o static/css/tailwind.css --minify",
  "watch": "tailwindcss build -o static/css/tailwind.css --watch"
}

The build script compiles Tailwind CSS stylesheet for production, and the watch script watches over changes to the content directory (view/**/*.templ) we configured above and automatically recompiles the stylesheet.

File: package.json

{
  "name": "mytempl",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tailwindcss build -o static/css/tailwind.css --minify",
    "watch": "tailwindcss build -o static/css/tailwind.css --watch"
  },
  "devDependencies": {
    "tailwindcss": "^3.4.1"
  }
}

Result

We have successfully set up a project with TEMPL, HTMX, and Tailwind CSS and configured Tailwind to automatically rebuild the stylesheet. However, there is one inconvenience that we still have to face during development - we need to restart the Go app every time we make a change to our project. Let's fix that.

Air

Moving on to the final piece of the article, Air is a program that allows us to live reload the Go app. To set it up with TEMPL, we need to create a .air.toml file in the root of the project.

# file: .air.toml

root = "."
tmp_dir = "bin"

[build]
  bin = "./bin/main"
  cmd = "templ generate && go build -o ./bin/main ."
  delay = 1000
  exclude_dir = ["static", "node_modules"]
  exclude_regex = [".*_templ.go"]
  exclude_unchanged = false
  follow_symlink = false
  include_ext = ["go", "tpl", "tmpl", "templ", "html"]
  kill_delay = "0s"
  log = "build-errors.log"
  send_interrupt = false
  stop_on_error = true

[color]
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  time = false

[misc]
  clean_on_exit = true

To run Air, all you need to do is type air. Air will automatically pick up the toml file and run the Go app.

air

Ending