A multi-page project demonstrating advanced Go templates, HTMX interactions, and Tailwind styling.
In this tutorial, we’ll build a multi-page Go application that uses:
{{.Username}}).hx-target.range in a Go template to display multiple repeated snippets.We won’t discuss why you might choose these technologies — we’ll simply show how to integrate them for a richer user experience.
Below is one suggested folder structure:
my-complex-htmx/
├── go.mod
├── main.go
└── templates/
├── base.html (layout file)
|── header.html
│── footer.html
├── index.html (home page)
├── custombutton.html (button snippet w/ .Username)
├── button1.html (first toggle state)
├── button2.html (second toggle state)
└── range.html (demonstrate {{range .}})
- base.html defines a main layout block.
- header.html and footer.html hold shared elements.
- index.html is the home page, referencing the layout.
- custombutton.html is for the button example that reveals server-sent data.
- button1.html / button2.html are toggled by HTMX.
- range.html uses range to repeat a snippet multiple times.
base.html
<!-- CODE BLOCK -->
{{define "base"}}
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{block "title" .}}My Complex HTMX App{{end}}</title>
<!-- Tailwind & HTMX -->
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.1/dist/tailwind.min.css"
rel="stylesheet"
/>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
</head>
<body class="bg-gray-900 text-teal-100 font-sans">
{{template "partials/header.html" .}}
<main class="container mx-auto px-4 py-8">
{{block "content" .}}{{end}}
</main>
</body>
</html>
{{end}}
This layout includes Tailwind (via CDN) and HTMX.
The main content block is injected with {{block "content" .}},
while header and footer partials are pulled in with {{template "partials/header.html" .}}
and {{template "partials/footer.html" .}}.
partials/header.html
<!-- CODE BLOCK -->
{{define "partials/header.html"}}
<header class="text-center py-4 border-b border-teal-700 mb-4">
<h1 class="text-2xl font-bold">My Complex HTMX Example</h1>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</header>
{{end}}
partials/footer.html
<!-- CODE BLOCK -->
{{define "partials/footer.html"}}
<footer class="text-center py-4 border-t border-teal-700 mt-4">
<p class="text-sm">© 2025 MyComplexHTMXApp. All rights reserved.</p>
</footer>
{{end}}
Our home page extends base.html. We add links/buttons that route the user
to different examples or triggers an HTMX call. This is just a placeholder to get started.
<!-- CODE BLOCK -->
{{define "base"}}
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>{{block "title" .}}My Complex HTMX App{{end}}</title>
<!-- Tailwind & HTMX -->
<link
href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.1/dist/tailwind.min.css"
rel="stylesheet"
/>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
</head>
<body class="bg-gray-900 text-teal-100 font-sans">
{{template "partials/header.html" .}}
<main class="container mx-auto px-4 py-8">
{{block "content" .}}{{end}}
</main>
</body>
</html>
{{end}}
When the user presses a button, an HTMX request fetches a Tailwind-styled snippet
that includes {{.Username}}. Let's define custombutton.html
as the snippet we’ll return from the server.
<!-- CODE BLOCK -->
{{define "custombutton"}}
<div class="bg-teal-800 text-white p-4 rounded shadow">
<p class="text-lg font-bold">Hello, {{.Username}}!</p>
<p>This element was loaded from the server via HTMX.</p>
</div>
{{end}}
We want two HTML snippets (button1.html and button2.html)
that swap when clicked. Each snippet has a button pointing to the opposite route.
<!-- CODE BLOCK -->
{{define "partials/footer.html"}}
<footer class="text-center py-4 border-t border-teal-700 mt-4">
<p class="text-sm">&copy; 2025 MyComplexHTMXApp. All rights reserved.</p>
</footer>
{{end}}
<!-- CODE BLOCK -->
{{define "button2"}}
<button
hx-get="/toggle1"
hx-swap="outerHTML"
class="bg-green-700 hover:bg-green-600 text-white px-4 py-2 rounded m-8"
>
I'm BUTTON2. Click to swap!
</button>
{{end}}
{{range .}} Operator
Suppose we want to show multiple repeated snippets. Let's define range.html
like this, using {{range .Items}} to iterate over a slice from Go:
<!-- CODE BLOCK -->
{{define "rangexample"}}
<div>
<h2 class="text-xl font-semibold mb-2">Range Example</h2>
<div class="bg-stone-800 rounded p-3 mb-2">
{{range . }}
<p class="text-sm text-stone-300">Username: {{ .Username }}</p>
<p class="text-sm text-stone-300">Item: {{ .Items }}</p>
{nd}}
</div>
</div>
{{end}}
{{range .}} example - array of elements
Suppose we want to show a snippet where one thing stays the same, and everything else changes. Let's define rangesecond.html
like this, using {{range .Items}} to iterate over another slice.:
<!-- CODE BLOCK -->
{{define "secondrange"}}
<div>
<h2 class="text-xl font-semibold mb-2">Range Example</h2>
<div class="bg-stone-800 rounded p-3 mb-2">
<p class="text-sm text-stone-300">Username: {{ .Username }}</p>
{{ range .Items }}
<p class="text-sm text-stone-300">Item: {{ . }}</p>
{{ end }}
</div>
</div>
{{end}}
Next, let’s show how to wire up these routes. Notice we have multiple handlers:
indexHandler: Renders index.html.customButtonHandler: Returns a page/snippet referencing custombutton.html.toggleHandler: Returns the initial toggle state (button1.html), plus /toggle1 and /toggle2 for the toggles themselves.rangeHandler: Renders range.html with a slice of items.
// code block
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
type UserData struct {
Username string
Items string
}
var tmpl *template.Template
func main() {
var err error
// Parse all .html files SUPER COMPLICATED GLOB PARSE
//Template matching is simple in Go, otherwise we'd have to use regex like: ("templates/[A-Za-z0-9]+/*[A-Za-z0-9]+(/[*])?/?.[A-Za-z0-9]+")
tmpl, err = template.ParseGlob("templates/*.html")
if err != nil {
log.Fatalf("Error parsing templates: %v", err)
}
// Routes
http.HandleFunc("/", indexHandler)
http.HandleFunc("/custombutton", customButtonHandler)
http.HandleFunc("/toggle", toggleHandler)
// For toggling:
http.HandleFunc("/toggle1", toggle1Handler)
http.HandleFunc("/toggle2", toggle2Handler)
http.HandleFunc("/range", rangeHandler)
http.HandleFunc("/extrarange", extrarangeHandler)
fmt.Println("Server running at http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
//4 hours(!)
}
}
// 1) Home page
func indexHandler(w http.ResponseWriter, r *http.Request) {
data := UserData{Username: "Guest"}
if err := tmpl.ExecuteTemplate(w, "index", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 2) Custom Button route
func customButtonHandler(w http.ResponseWriter, r *http.Request) {
data := UserData{Username: "Alice"} // The name we inject
if err := tmpl.ExecuteTemplate(w, "custombutton", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 3) Toggle route
func toggleHandler(w http.ResponseWriter, r *http.Request) {
// Return an initial button (button1)
if err := tmpl.ExecuteTemplate(w, "button2", nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func toggle1Handler(w http.ResponseWriter, r *http.Request) {
// Return button2 snippet
if err := tmpl.ExecuteTemplate(w, "button1", nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func toggle2Handler(w http.ResponseWriter, r *http.Request) {
// Return button1 snippet
if err := tmpl.ExecuteTemplate(w, "button2", nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 4) Range route
func rangeHandler(w http.ResponseWriter, r *http.Request) {
data2 := UserData{
Username: "TestUser2",
Items: "B",
}
data1 := UserData{
Username: "TestUser1", // It orders itself. Testuser1 will appear ahead of 2
Items: "A",
}
data3 := UserData{
Username: "TestUser3",
Items: "C",
}
netdata := []UserData{data1, data2, data3}
if err := tmpl.ExecuteTemplate(w, "rangexample", netdata); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func extrarangeHandler(w http.ResponseWriter, r *http.Request) {
//Another example - this time with a list of items but with the user being the same
type NewUserData struct {
Username string
Items []string
}
finaldata := NewUserData{
Username: "I_Got_Items!!",
Items: []string{"Water", "Bread", "Fish"},
}
if err := tmpl.ExecuteTemplate(w, "secondrange", finaldata); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
fmt.Printf("Range is read!")
}
//end code slice
1. In your project folder, run
go mod init to initialize the go code. This should create a go.mod with "module main" at the top
2. Run go run main.go to start the server.
3. Visit http://localhost:8080
in your browser.
4. Click on the links to test:
/custombutton.
You can wrap it in a button on another page that does hx-get="/custombutton"
or serve it directly.
/toggle starts with button1.html.
Clicking swaps to button2.html, and back again.
/range displays a repeated snippet for each item
in your .Items slice.
By combining Go templates (with a layout + partials), HTMX for dynamic requests, and Tailwind for styling, you can build a multi-page site that feels both modern and well-structured. Each snippet or page can independently use Tailwind’s utility classes for consistent styling, while HTMX updates parts of the DOM with minimal fuss. Enjoy building your advanced GOTTH stack!
You did it.
I have nothing more to teach you (for now). Go out and make mistakes!