Getting Started
Build a filterable, sortable table of dog breeds with Go + Templ + HTMX + Alpine.js. No Node.js, no bundlers.
1. Create your project
bash
mkdir dog-breeds && cd dog-breeds
go mod init dog-breeds
go get github.com/a-h/templ@latest
go get github.com/araihu/goshtoso@latest
# Extract Goshtoso CSS (themes, component styles, utilities)
go run github.com/araihu/goshtoso/cmd/goshtoso@latest -out=goshtoso.css2. Create the page template
Create page.templ — Alpine.js handles the filter bar state, HTMX loads and swaps table rows from the server:
page.templ
package main
import "github.com/araihu/goshtoso/components/table"
// Page renders the dog breeds page using the Goshtoso table component.
// Filtering, sorting, and pagination are all built into the component —
// just configure them via table.Config.
//
// Assets (CSS, Alpine.js, HTMX) are served from /assets/ via the embedded
// assets.Handler() — no CDN needed, works fully offline.
templ Page(cfg table.Config) {
<!DOCTYPE html>
<html lang="en" x-data x-init="
const t = localStorage.getItem('theme') || 'minimal';
document.documentElement.setAttribute('data-theme', t);
if (localStorage.getItem('darkMode') === 'true') document.documentElement.classList.add('dark');
">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Dog Breeds — Goshtoso Getting Started</title>
<!-- Embedded Goshtoso assets (CSS with themes + JS) -->
<link rel="stylesheet" href="/assets/styles.css"/>
<script defer src="/assets/js/vendor/alpine-collapse.min.js"></script>
<script defer src="/assets/js/vendor/alpine-focus.min.js"></script>
<script defer src="/assets/js/vendor/alpine.min.js"></script>
<script src="/assets/js/vendor/htmx.min.js"></script>
<script src="/assets/js/darkmode.js"></script>
<style>[x-cloak] { display: none !important; }</style>
</head>
<body class="min-h-screen bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark">
<div class="max-w-5xl mx-auto px-6 py-12">
<h1 class="text-3xl font-bold font-title mb-2 text-on-surface-strong dark:text-on-surface-dark-strong">Dog Breeds</h1>
<p class="text-on-surface-muted dark:text-on-surface-dark-muted mb-8">
A filterable, sortable, paginated table built with
<a href="https://github.com/araihu/goshtoso" class="underline text-primary dark:text-primary-dark">Goshtoso</a> —
Go + Alpine.js + Tailwind CSS + Templ + HTMX.
</p>
@table.Table(cfg)
<p class="mt-6 text-xs text-on-surface-muted dark:text-on-surface-dark-muted text-center">
Built with <a href="https://github.com/araihu/goshtoso" class="underline">Goshtoso</a>
</p>
</div>
</body>
</html>
}
3. Create the server
Create main.go — serves the page and handles the HTMX API endpoint for filtering, searching, and sorting:
main.go
package main
import (
"fmt"
"log"
"net/http"
"sort"
"strconv"
"strings"
"github.com/araihu/goshtoso/assets"
"github.com/araihu/goshtoso/components/table"
)
// Dog represents a dog breed with metadata
type Dog struct {
Breed string
Group string
Origin string
Size string
Temperament string
}
var breeds = []Dog{
{Breed: "Labrador Retriever", Group: "Sporting", Origin: "Canada", Size: "Large", Temperament: "Friendly"},
{Breed: "German Shepherd", Group: "Herding", Origin: "Germany", Size: "Large", Temperament: "Loyal"},
{Breed: "Golden Retriever", Group: "Sporting", Origin: "Scotland", Size: "Large", Temperament: "Gentle"},
{Breed: "French Bulldog", Group: "Non-Sporting", Origin: "France", Size: "Small", Temperament: "Playful"},
{Breed: "Bulldog", Group: "Non-Sporting", Origin: "England", Size: "Medium", Temperament: "Calm"},
{Breed: "Poodle", Group: "Non-Sporting", Origin: "Germany", Size: "Medium", Temperament: "Intelligent"},
{Breed: "Beagle", Group: "Hound", Origin: "England", Size: "Small", Temperament: "Curious"},
{Breed: "Rottweiler", Group: "Working", Origin: "Germany", Size: "Large", Temperament: "Confident"},
{Breed: "Dachshund", Group: "Hound", Origin: "Germany", Size: "Small", Temperament: "Clever"},
{Breed: "Yorkshire Terrier", Group: "Toy", Origin: "England", Size: "Small", Temperament: "Spirited"},
{Breed: "Boxer", Group: "Working", Origin: "Germany", Size: "Large", Temperament: "Energetic"},
{Breed: "Siberian Husky", Group: "Working", Origin: "Russia", Size: "Medium", Temperament: "Outgoing"},
{Breed: "Shih Tzu", Group: "Toy", Origin: "China", Size: "Small", Temperament: "Affectionate"},
{Breed: "Border Collie", Group: "Herding", Origin: "Scotland", Size: "Medium", Temperament: "Smart"},
{Breed: "Doberman", Group: "Working", Origin: "Germany", Size: "Large", Temperament: "Alert"},
{Breed: "Corgi", Group: "Herding", Origin: "Wales", Size: "Small", Temperament: "Happy"},
{Breed: "Australian Shepherd", Group: "Herding", Origin: "USA", Size: "Medium", Temperament: "Active"},
{Breed: "Cavalier King Charles", Group: "Toy", Origin: "England", Size: "Small", Temperament: "Graceful"},
{Breed: "Great Dane", Group: "Working", Origin: "Germany", Size: "Large", Temperament: "Patient"},
{Breed: "Chihuahua", Group: "Toy", Origin: "Mexico", Size: "Small", Temperament: "Charming"},
}
// columns defines the table headers
func columns() []table.Column {
return []table.Column{
{Key: "breed", Label: "Breed", Sortable: true},
{Key: "group", Label: "Group", Sortable: true},
{Key: "origin", Label: "Origin", Sortable: true},
{Key: "size", Label: "Size", Sortable: true},
{Key: "temperament", Label: "Temperament"},
}
}
// filters defines the built-in filter bar controls
func filters() *table.FilterConfig {
return &table.FilterConfig{
Collapsible: true,
InitiallyExpanded: true,
Filters: []table.Filter{
{
Key: "search",
Label: "Search",
Type: table.FilterSearch,
Placeholder: "Search breeds, origins, temperaments...",
},
{
Key: "group",
Label: "Group",
Type: table.FilterSelect,
Options: []table.FilterOption{
{Value: "", Label: "All Groups"},
{Value: "Sporting", Label: "Sporting"},
{Value: "Herding", Label: "Herding"},
{Value: "Hound", Label: "Hound"},
{Value: "Working", Label: "Working"},
{Value: "Non-Sporting", Label: "Non-Sporting"},
{Value: "Toy", Label: "Toy"},
},
},
},
}
}
// dogsToRows converts Dogs into table.Row
func dogsToRows(dogs []Dog) []table.Row {
rows := make([]table.Row, len(dogs))
for i, d := range dogs {
rows[i] = table.Row{
ID: d.Breed,
Cells: map[string]table.Cell{
"breed": {Text: d.Breed},
"group": {Text: d.Group},
"origin": {Text: d.Origin},
"size": {Text: d.Size},
"temperament": {Text: d.Temperament},
},
}
}
return rows
}
// filterAndSort applies search, group filter, and sort to the breed list
func filterAndSort(search, group, orderBy, orderDir string) []Dog {
var filtered []Dog
search = strings.ToLower(search)
for _, d := range breeds {
if group != "" && d.Group != group {
continue
}
if search != "" &&
!strings.Contains(strings.ToLower(d.Breed), search) &&
!strings.Contains(strings.ToLower(d.Origin), search) &&
!strings.Contains(strings.ToLower(d.Temperament), search) {
continue
}
filtered = append(filtered, d)
}
if orderBy != "" {
sort.SliceStable(filtered, func(i, j int) bool {
var a, b string
switch orderBy {
case "breed":
a, b = filtered[i].Breed, filtered[j].Breed
case "group":
a, b = filtered[i].Group, filtered[j].Group
case "origin":
a, b = filtered[i].Origin, filtered[j].Origin
case "size":
a, b = filtered[i].Size, filtered[j].Size
}
if orderDir == "desc" {
return a > b
}
return a < b
})
}
return filtered
}
func main() {
const perPage = 5
mux := http.NewServeMux()
// Serve embedded Goshtoso assets (CSS with themes, Alpine.js, HTMX, fonts)
mux.Handle("/assets/", assets.Handler())
// Main page — renders the full table with filters, sorting, and pagination
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
dogs := filterAndSort("", "", "breed", "asc")
totalPages := (len(dogs) + perPage - 1) / perPage
pageRows := dogsToRows(dogs[:min(perPage, len(dogs))])
Page(table.Config{
ID: "breeds",
HTMXEndpoint: "/api/breeds",
Columns: columns(),
Rows: pageRows,
SortBy: "breed",
SortDir: table.SortAsc,
Pagination: &table.PaginationConfig{CurrentPage: 1, TotalPages: totalPages, PerPage: perPage},
Filters: filters(),
}).Render(r.Context(), w)
})
// HTMX endpoint — returns filtered/sorted/paginated table rows
mux.HandleFunc("/api/breeds", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
q := r.URL.Query()
search := q.Get("search")
group := q.Get("group")
orderBy := q.Get("order_by")
orderDir := q.Get("order_dir")
if orderDir == "" {
orderDir = "asc"
}
dogs := filterAndSort(search, group, orderBy, orderDir)
// Pagination
page := 1
pp := perPage
if v := q.Get("page"); v != "" {
if p, err := strconv.Atoi(v); err == nil && p > 0 {
page = p
}
}
if v := q.Get("per_page"); v != "" {
if p, err := strconv.Atoi(v); err == nil && p > 0 {
pp = p
}
}
totalPages := (len(dogs) + pp - 1) / pp
start := (page - 1) * pp
if start >= len(dogs) {
start = 0
page = 1
}
end := start + pp
if end > len(dogs) {
end = len(dogs)
}
cfg := table.Config{
ID: "breeds",
Columns: columns(),
Rows: dogsToRows(dogs[start:end]),
HTMXEndpoint: "/api/breeds",
SortBy: orderBy,
SortDir: table.SortDir(orderDir),
Pagination: &table.PaginationConfig{CurrentPage: page, TotalPages: totalPages, PerPage: pp},
}
// Render table rows
for _, row := range cfg.Rows {
table.TableRow(cfg, row).Render(r.Context(), w)
}
// OOB: update pagination controls
if totalPages > 1 {
fmt.Fprintf(w, `<div id="%s" hx-swap-oob="true" class="flex items-center justify-between border-t border-gray-200 px-4 py-3">`, cfg.PaginationID())
fmt.Fprintf(w, `<div class="text-sm text-gray-500">Page %d of %d</div>`, page, totalPages)
table.TablePaginationNav(cfg).Render(r.Context(), w)
fmt.Fprintf(w, `</div>`)
}
})
addr := ":3000"
log.Printf("Dog breeds app running at http://localhost%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
4. Generate and run
bash
templ generate
go run .
# Open http://localhost:3000What you get
- • 20 dog breeds in a sortable table — click any column header
- • Live search with 300ms debounce via Alpine.js
x-model - • Group filter dropdown (Sporting, Herding, Working, Hound, Non-Sporting, Toy)
- • Server-rendered HTML fragments via HTMX — no JSON, no fetch, no client state
- • Single Go binary, zero JavaScript build tools
Full source: examples/getting-started