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!