Catuaba uses Templ for type-safe, compiled HTML templates. Views live in app/views/ and are organized by resource.
Templ files (.templ) are compiled to Go code at build time. They provide:
- Type safety — template arguments are Go types, caught at compile time
- IDE support — autocomplete, go-to-definition, error highlighting
- Performance — 5-10x faster than
html/template - Composability — components call other components like functions
app/views/
├── layouts/
│ └── base.templ # HTML5 base layout (head, nav, flash, footer)
├── components/
│ ├── nav.templ # Navigation bar
│ ├── flash.templ # Flash messages (auto-dismiss with Alpine.js)
│ ├── pagination.templ # Pagination with HTMX
│ └── form_field.templ # Reusable form field
├── pages/
│ ├── home.templ # Landing page
│ └── not_found.templ # 404 page
└── {resource}/ # Generated by scaffold
├── index.templ # List/table view
├── show.templ # Detail view
└── form.templ # Create/edit form (shared)
Every page uses the base layout, which provides the HTML shell, navigation, flash messages, and footer:
// app/views/layouts/base.templ
templ Base(title string, appName string, flashMsg string, flashType string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title } - { appName }</title>
<link rel="stylesheet" href="/static/css/app.css"/>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
</head>
<body class="min-h-screen bg-gray-50" hx-boost="true">
@components.Nav(appName)
if flashMsg != "" {
@components.Flash(flashMsg, flashType)
}
<main class="flex-1">
{ children... }
</main>
<footer>...</footer>
</body>
</html>
}Use it in any view:
templ MyPage() {
@layouts.Base("Page Title", "", "", "") {
<h1>Hello!</h1>
}
}catuaba g scaffold Post title:string body:text published:boolean generates three views:
A table with all records, pagination, and action links:
templ Index(items []models.Post, page int, totalPages int, flashMsg string, flashType string) {
@layouts.Base("Posts", "", flashMsg, flashType) {
// Header with "New Post" button
// Table with columns for each field
// Pagination component
}
}Features:
- Pagination — page/limit from query params, rendered with the
Paginationcomponent - Boolean fields — displayed as colored badges ("Yes" / "No")
- Text fields — truncated in the table, full text on show page
- Action links — View and Edit for each row
A detail view with all fields and Edit/Delete actions:
templ Show(item models.Post, flashMsg string, flashType string) {
@layouts.Base("Post Details", "", flashMsg, flashType) {
// Back link
// Card with field labels and values
// Edit button + Delete form with confirmation
// CreatedAt and UpdatedAt timestamps
}
}A shared form for both creating and editing:
templ Form(item models.Post, isEdit bool, errors map[string]string, flashMsg string, flashType string) {
@layouts.Base(formTitle(isEdit), "", flashMsg, flashType) {
// Error display (if any)
// Form fields based on type:
// string → <input type="text">
// text → <textarea>
// integer/float → <input type="number">
// boolean → <input type="hidden" value="false"> + <input type="checkbox">
// datetime → <input type="datetime-local">
// Submit button (Create / Update)
}
}The isEdit flag controls the form action URL and button label. Helper functions formTitle() and formAction() are defined at the bottom of the file.
Boolean checkbox pattern: A hidden input with value="false" is placed before the checkbox so unchecked state sends false to the server.
Auto-dismiss after 5 seconds using Alpine.js:
@components.Flash("Post created!", "success")Types: success (green), error (red), warning (yellow).
HTMX-powered pagination (partial page updates):
@components.Pagination(currentPage, totalPages, "/posts")Responsive navbar with optional auth links (when --auth is used).
The base layout includes:
hx-boost="true"on<body>— all links and forms use AJAX by default- CSRF token injection via
htmx:configRequestevent — sends theX-CSRF-Tokenheader automatically - Pagination uses
hx-getandhx-target="#content"for partial page updates
You can add HTMX attributes to any element:
// Inline delete with confirmation
<button
hx-post={ fmt.Sprintf("/posts/%d/delete", item.ID) }
hx-confirm="Are you sure?"
hx-target="#content"
hx-swap="outerHTML"
class="text-red-600 hover:underline"
>
Delete
</button>
// Live search
<input
type="search"
name="q"
hx-get="/posts"
hx-trigger="keyup changed delay:300ms"
hx-target="#content"
placeholder="Search posts..."
/>Alpine.js is included for client-side interactivity:
// Toggle visibility
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>Content here</div>
</div>
// Dropdown menu
<div x-data="{ open: false }" @click.outside="open = false">
<button @click="open = !open">Menu</button>
<div x-show="open" class="absolute mt-2 bg-white shadow rounded">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
</div>For pages that don't belong to a resource:
catuaba g view aboutCreates app/views/pages/about.templ with the base layout.
Views use Tailwind CSS classes directly in the HTML. The CSS is compiled from static/css/input.css to static/css/app.css by the Tailwind CLI.
During development, make dev runs the Tailwind watcher that recompiles on save.
- Edit
.templfiles - Templ watcher recompiles to
_templ.gofiles - wgo detects the Go file change and rebuilds the server
- Browser refreshes (or HTMX swaps the content)
All three watchers run in parallel with make dev.