Fuego uses templ for type-safe HTML templates. Templ provides compile-time validation and excellent Go integration.
Why templ?
- Type-safe - Compile-time checking catches errors early
- Fast - No runtime template parsing
- Go-native - Full Go code completion in templates
- Small - Smaller binary size than text/template
File Conventions
| File | Purpose | Route |
|---|
page.templ | Page content | URL path |
layout.templ | Wraps pages | Inherited by children |
Creating Pages
Basic Page
Create app/dashboard/page.templ:
package dashboard
templ Page() {
<div class="container">
<h1>Dashboard</h1>
<p>Welcome to your dashboard!</p>
</div>
}
This maps to /dashboard.
Page with Props
package dashboard
type PageProps struct {
User User
Stats DashboardStats
}
templ Page(props PageProps) {
<div class="container">
<h1>Welcome, { props.User.Name }!</h1>
<div class="stats">
<p>Total tasks: { fmt.Sprint(props.Stats.TaskCount) }</p>
</div>
</div>
}
Creating Layouts
Root Layout
Create app/layout.templ:
package app
templ Layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title } - My App</title>
<link rel="stylesheet" href="/docs/static/css/output.css"/>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4">
<a href="/docs/" class="text-xl font-bold">My App</a>
</div>
</nav>
<main class="max-w-7xl mx-auto py-6">
{ children... }
</main>
</body>
</html>
}
Layouts must include { children... } where page content should appear.
Nested Layouts
Create app/dashboard/layout.templ:
package dashboard
templ Layout() {
<div class="flex">
<aside class="w-64 bg-gray-800 text-white">
<nav>
<a href="/docs/dashboard">Overview</a>
<a href="/docs/dashboard/settings">Settings</a>
</nav>
</aside>
<div class="flex-1">
{ children... }
</div>
</div>
}
Nested layouts override parent layouts.
Dynamic Pages
Dynamic pages use bracket notation in directory names to capture URL parameters.
URL Parameters
Create app/posts/[slug]/page.templ:
package slug
templ Page(slug string) {
<article>
<h1>Post: { slug }</h1>
<div hx-get={ "/api/posts/" + slug } hx-trigger="load">
Loading content...
</div>
</article>
}
This maps to /posts/:slug. The slug parameter is automatically extracted from the URL and passed to your Page() function.
Parameter Matching
Fuego matches URL parameters to Page() parameters by name. Your parameter names should match the bracket directory names:
| Directory | Page Signature | Match |
|---|
[slug] | Page(slug string) | ✓ |
[id] | Page(id string) | ✓ |
[id] | Page(userId string) | ⚠️ Warning |
If parameter names don’t match, Fuego shows a warning during route generation. The page still renders, but mismatched parameters receive zero values.
Multiple Parameters
For nested dynamic routes:
// app/orgs/[orgId]/users/[userId]/page.templ
package userId
templ Page(orgId, userId string) {
<div>
<p>Organization: { orgId }</p>
<p>User: { userId }</p>
</div>
}
This maps to /orgs/:orgId/users/:userId.
Catch-All Routes
Use [...param] for catch-all routes:
// app/docs/[...slug]/page.templ
package slug
templ Page(slug string) {
<div>
<p>Path: { slug }</p>
</div>
}
This matches /docs/anything/here/deeply/nested with slug = "anything/here/deeply/nested".
Pages with Additional Props
You can accept additional parameters beyond URL params. Non-URL parameters receive zero values unless you fetch data in a custom handler:
// app/users/[id]/page.templ
package id
type User struct {
ID string
Name string
Email string
}
templ Page(id string, user User) {
<div class="user-profile">
if user.Name != "" {
<h1>{ user.Name }</h1>
<p>{ user.Email }</p>
} else {
<p>Loading user { id }...</p>
}
</div>
}
For pages that need to load data before rendering, consider using HTMX to load content dynamically, or create a custom route handler.
Generated Symlinks
Due to Go’s restriction on brackets in import paths, Fuego creates temporary symlinks for bracket directories:
app/posts/[slug]/ → app/posts/_slug/ (symlink)
These symlinks are:
- Created automatically during
fuego dev and fuego build
- Should be added to
.gitignore
- Cleaned up when the dev server stops
Components
Creating Components
package components
templ Button(text string, variant string) {
<button class={ "btn", "btn-" + variant }>
{ text }
</button>
}
templ Card(title string) {
<div class="card">
<h2>{ title }</h2>
<div class="card-body">
{ children... }
</div>
</div>
}
Using Components
package dashboard
import "myapp/app/_components"
templ Page() {
<div>
@components.Card("Statistics") {
<p>Your stats here</p>
}
@components.Button("Save", "primary")
</div>
}
HTMX Integration
Loading Data
templ TaskList() {
<div id="task-list" hx-get="/api/tasks" hx-trigger="load">
<p>Loading tasks...</p>
</div>
}
templ AddTaskForm() {
<form
hx-post="/api/tasks"
hx-target="#task-list"
hx-swap="innerHTML"
hx-on::after-request="this.reset()"
>
<input type="text" name="title" placeholder="New task..." required/>
<button type="submit">Add Task</button>
</form>
}
Toggle Actions
templ TaskItem(task Task) {
<li class="flex items-center gap-3">
<input
type="checkbox"
hx-post={ fmt.Sprintf("/api/tasks/toggle?id=%d", task.ID) }
hx-target="#task-list"
if task.Completed {
checked
}
/>
<span class={ templ.KV("line-through", task.Completed) }>
{ task.Title }
</span>
<button
hx-delete={ fmt.Sprintf("/api/tasks?id=%d", task.ID) }
hx-target="#task-list"
>
Delete
</button>
</li>
}
Full Example: Task Dashboard
// app/dashboard/page.templ
package dashboard
templ Page() {
<div class="px-4 py-5 sm:p-6">
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
<div class="bg-white shadow rounded-lg p-6 mb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">Tasks</h2>
<!-- HTMX-powered task list -->
<div id="task-list" hx-get="/api/tasks" hx-trigger="load" hx-swap="innerHTML">
<div class="text-gray-500">Loading tasks...</div>
</div>
<!-- Add task form with HTMX -->
<form
class="mt-6 flex gap-4"
hx-post="/api/tasks"
hx-target="#task-list"
hx-swap="innerHTML"
hx-on::after-request="this.reset()"
>
<input
type="text"
name="title"
placeholder="New task..."
class="flex-1 rounded-md border-gray-300"
required
/>
<button
type="submit"
class="px-4 py-2 bg-orange-600 text-white rounded-md"
>
Add Task
</button>
</form>
</div>
</div>
}
Generating Pages
Use the CLI to generate pages:
# Basic page
fuego generate page dashboard
# Page with layout
fuego generate page admin/settings --with-layout
# Nested page
fuego generate page users/profile
Best Practices
Use components for reusability
Extract common UI patterns into components in a _components folder.
Pages should primarily compose components, not contain complex logic.
Use HTMX for interactivity
Let the server render HTML instead of writing client-side JavaScript.
templ catches errors during compilation. Fix warnings before deploying.
Next Steps