templ¶
HTML templates that are compiled rather than parsed. You write .templ
files, a generator produces .go, and a typo in a field name becomes a
compile error instead of a blank page.
Module:
github.com/a-h/templ, plus thetemplCLI.
Compare with templates,
which does the same job with html/template.
The pitch¶
html/template resolves {{.Name}} at runtime. Rename the field and
you find out when the page renders — or when that branch renders,
which may be in production.
templ generates Go. i.Name is a real field access, so the compiler
checks it, your editor completes it, and renaming with a refactoring
tool updates the template.
The syntax¶
A templ block is a function returning a component:
package main
import "fmt"
templ itemRow(i Item) {
<li class="row">
<span>{ i.Name }</span>
<span>{ fmt.Sprintf("$%.2f", i.Price) }</span>
if len(i.Tags) > 0 {
<em>{ fmt.Sprint(len(i.Tags)) } tags</em>
} else {
<em>untagged</em>
}
</li>
}
Braces interpolate a Go expression returning a string. Not a
template language — real Go, with real imports. That is why the price
formatting is fmt.Sprintf rather than a custom template function.
if, for and switch are Go keywords with Go syntax, so there is no
second dialect to learn and no {{end}} to forget.
@name(args) renders another component.
Composition with children...¶
A layout takes the page content as children:
templ layout(title string) {
<html>
<head><title>{ title }</title></head>
<body>
{ children... }
</body>
</html>
}
templ ItemList(title string, items []Item) {
@layout(title) {
<ul>
for _, i := range items {
@itemRow(i)
}
</ul>
}
}
The block after @layout(title) becomes its children. This replaces
define/block from html/template, and it composes like ordinary
function calls.
Generating¶
Produces page_templ.go next to each .templ, headed:
Commit the generated files. That way the repo builds with plain
go build and CI does not need the CLI — the reasoning from
build and codegen.
Wire templ generate into your Makefile, and have CI regenerate and
fail on a diff so nobody forgets.
templ generate --watch regenerates on save during development.
Rendering¶
A component renders to any io.Writer:
Which in a net/http handler is:
func (h Handler) list(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := ItemList("Shop", items).Render(r.Context(), w); err != nil {
http.Error(w, "render failed", http.StatusInternalServerError)
}
}
There is a catch worth knowing: Render streams, so it may have
written part of the page before failing — and the status is already
200. If that matters, render into a bytes.Buffer first and copy it
out only on success.
Escaping¶
Contextual, like html/template:
Text is escaped, attributes are escaped, and templ.URL sanitises a
URL in an href. A dangerous scheme is replaced outright:
"/x?q=1&b=2" <a href="/x?q=1&b=2">link</a>
"https://example.com/a" <a href="https://example.com/a">link</a>
"mailto:a@b.c" <a href="mailto:a@b.c">link</a>
"javascript:alert(1)" <a href="about:invalid#TemplFailedSanitizationURL">link</a>
"data:text/html,<script>1" <a href="about:invalid#TemplFailedSanitizationURL">link</a>
about:invalid#TemplFailedSanitizationURL is templ's equivalent of
html/template's ZgotmplZ marker — finding it in a rendered page
means unsafe content reached a URL position, and the fix is in the data
rather than the template.
templ.Raw opts out of escaping and should only ever see content you
produced.
Because escaping happens at generation time in known positions, it is harder to accidentally bypass than in a string-based template.
View models¶
Do not hand your database models to a template. A dedicated view type keeps display logic out of both:
The handler maps domain to view, the template renders the view. Pure
mapping functions are easy to test, and the template stays free of
if user.Role == "admin" && !user.Suspended conditionals.
Testing¶
A component is a function returning something with a Render method,
so tests are plain Go:
var sb strings.Builder
err := ItemList("t", items).Render(context.Background(), &sb)
// then assert on sb.String()
Assert on structure rather than exact bytes — a golden file from helpers and golden files works well here, since a whole page is too big to inline.
Costs¶
- A build step. Editing HTML now means regenerating, and a stale generated file is a confusing bug.
- Editor support is separate. There is an LSP and plugins, but it
is not the out-of-the-box experience of
.html. - The generated code is noisy in diffs. Some teams gitignore it and
generate in CI instead — at the cost of
go buildno longer working alone. - It is a different language in the same file. Designers editing templates need to learn it.
Against that: a renamed field cannot silently break a page, and there is no runtime template parsing at all.
From Python: this is Jinja replaced by something closer to a JSX compiler — templates become typed functions checked at build time. The trade is the same one: type safety and editor support against a compile step and a format non-programmers find less approachable.
Quick reference¶
| Task | Form |
|---|---|
| a component | templ Name(args) { ... } in a .templ file |
| interpolate | { goExpression } — real Go, returning a string |
| control flow | Go if, for, switch — no {{end}} |
| call another | @other(arg) |
| layouts | { children... } plus @layout(x) { ... } |
| generate | templ generate, --watch while developing |
| commit | yes — the generated _templ.go files |
| render | .Render(ctx, w) to any io.Writer |
| safe URLs | templ.URL(...); templ.Raw only for your own HTML |
| test | render into a strings.Builder, assert or use a golden file |