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!