Instruction file imported from CoreyCole/datastarui (
.cursor/rules/templ.mdc). Copyright stays with the author.
Injection attacks
templ is designed to prevent user-provided data from being used to inject vulnerabilities.
<script> and <style> tags could allow user data to inject vulnerabilities, so variables are not permitted in these sections.
templ Example() {
<script>
function showAlert() {
alert("hello");
}
</script>
<style type="text/css">
/* Only CSS is allowed */
</style>
}
onClick attributes, and other on* attributes are used to execute JavaScript. To prevent user data from being unescaped, on* attributes accept a templ.ComponentScript.
script onClickHandler(msg string) {
alert(msg);
}
templ Example(msg string) {
<div onClick={ onClickHandler(msg) }>
{ "will be HTML encoded using templ.Escape" }
</div>
}
Style attributes cannot be expressions, only constants, to avoid escaping vulnerabilities. templ style templates (css className()) should be used instead.
templ Example() {
<div style={ "will throw an error" }></div>
}
Class names are sanitized by default. A failed class name is replaced by --templ-css-class-safe-name. The sanitization can be bypassed using the templ.SafeClass function, but the result is still subject to escaping.
templ Example() {
<div class={ "unsafe</style>-will-sanitized", templ.SafeClass("&sanitization bypassed") }></div>
}
Rendered output:
<div class="--templ-css-class-safe-name &sanitization bypassed"></div>
templ Example() {
<div>Node text is not modified at all.</div>
<div>{ "will be escaped using templ.EscapeString" }</div>
}
href attributes must be a templ.SafeURL and are sanitized to remove JavaScript URLs unless bypassed.
templ Example() {
<a href="http://constants.example.com/are/not/sanitized">Text</a>
<a href={ templ.URL("will be sanitized by templ.URL to remove potential attacks") }</a>
<a href={ templ.SafeURL("will not be sanitized by templ.URL") }</a>
}
Within css blocks, property names, and constant CSS property values are not sanitized or escaped.
css className() {
background-color: #ffffff;
}
CSS property values based on expressions are passed through templ.SanitizeCSS to replace potentially unsafe values with placeholders.
css className() {
color: { red };
}
Content security policy
Nonces
In templ script templates are rendered as inline <script> tags.
Strict Content Security Policies (CSP) can prevent these inline scripts from executing.
By setting a nonce attribute on the <script> tag, and setting the same nonce in the CSP header, the browser will allow the script to execute.
:::info It's your responsibility to generate a secure nonce. Nonces should be generated using a cryptographically secure random number generator.
See https://content-security-policy.com/nonce/ for more information. :::
Setting a nonce
The templ.WithNonce function can be used to set a nonce for templ to use when rendering scripts.
It returns an updated context.Context with the nonce set.
In this example, the alert function is rendered as a script element by templ.
package main
import "context"
import "os"
script onLoad() {
alert("Hello, world!")
}
templ template() {
@onLoad()
}
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func withNonce(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonce := securelyGenerateRandomString()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
// Use the context to pass the nonce to the handler.
ctx := templ.WithNonce(r.Context(), nonce)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
mux := http.NewServeMux()
// Handle template.
mux.HandleFunc("/", templ.Handler(template()))
// Apply middleware.
withNonceMux := withNonce(mux)
// Start the server.
fmt.Println("listening on :8080")
if err := http.ListenAndServe(":8080", withNonceMux); err != nil {
log.Printf("error listening: %v", err)
}
}
<script nonce="randomly generated nonce">
function __templ_onLoad_5a85() {
alert("Hello, world!")
}
</script>
<script nonce="randomly generated nonce">
__templ_onLoad_5a85()
</script>
Code signing
Binaries are created by the GitHub Actions workflow at https://github.com/a-h/templ/blob/main/.github/workflows/release.yml
Binaries are signed by cosign. The public key is stored in the repository at https://github.com/a-h/templ/blob/main/cosign.pub
Instructions for key verification at https://docs.sigstore.dev/verifying/verify/
sidebar_position: 1
Introduction
templ - build HTML with Go
Create components that render fragments of HTML and compose them to create screens, pages, documents, or apps.
- Server-side rendering: Deploy as a serverless function, Docker container, or standard Go program.
- Static rendering: Create static HTML files to deploy however you choose.
- Compiled code: Components are compiled into performant Go code.
- Use Go: Call any Go code, and use standard
if,switch, andforstatements. - No JavaScript: Does not require any client or server-side JavaScript.
- Great developer experience: Ships with IDE autocompletion.
package main
templ Hello(name string) {
<div>Hello, { name }</div>
}
templ Greeting(person Person) {
<div class="greeting">
@Hello(person.Name)
</div>
}
Getting help
For help from the community, talking about new ideas, and general discussion:
Slack
Use the #templ channel in the Gopher Slack community.
https://invite.slack.golangbridge.org/
GitHub Discussion
https://github.com/a-h/templ/discussions
CLI
templ provides a command line interface. Most users will only need to run the templ generate command to generate Go code from *.templ files.
usage: templ <command> [<args>...]
templ - build HTML UIs with Go
See docs at https://templ.guide
commands:
generate Generates Go code from templ files
fmt Formats templ files
lsp Starts a language server for templ files
info Displays information about the templ environment
version Prints the version
Generating Go code from templ files
The templ generate command generates Go code from *.templ files in the current directory tree.
The command provides additional options:
usage: templ generate [<args>...]
Generates Go code from templ files.
Args:
-path <path>
Generates code for all files in path. (default .)
-f <file>
Optionally generates code for a single file, e.g. -f header.templ
-source-map-visualisations
Set to true to generate HTML files to visualise the templ code and its corresponding Go code.
-include-version
Set to false to skip inclusion of the templ version in the generated code. (default true)
-include-timestamp
Set to true to include the current time in the generated code.
-watch
Set to true to watch the path for changes and regenerate code.
-cmd <cmd>
Set the command to run after generating code.
-proxy
Set the URL to proxy after generating code and executing the command.
-proxyport
The port the proxy will listen on. (default 7331)
-proxybind
The address the proxy will listen on. (default 127.0.0.1)
-w
Number of workers to use when generating code. (default runtime.NumCPUs)
-lazy
Only generate .go files if the source .templ file is newer.
-pprof
Port to run the pprof server on.
-keep-orphaned-files
Keeps orphaned generated templ files. (default false)
-v
Set log verbosity level to "debug". (default "info")
-log-level
Set log verbosity level. (default "info", options: "debug", "info", "warn", "error")
-help
Print help and exit.
For example, to generate code for a single file:
templ generate -f header.templ
Formatting templ files
The templ fmt command formats template files. You can use this command in different ways:
- Format all template files in the current directory and subdirectories:
templ fmt .
- Format input from stdin and output to stdout:
templ fmt
Alternatively, you can run fmt in CI to ensure that invalidly formatted templatess do not pass CI. This will cause the command
to exit with unix error-code 1 if any templates needed to be modified.
templ fmt -fail .
Language Server for IDE integration
templ lsp provides a Language Server Protocol (LSP) implementation to support IDE integrations.
This command isn't intended to be used directly by users, but is used by IDE integrations such as the VSCode extension and by Neovim support.
By default, templ lsp starts its own instance of gopls. However, gopls supports a shared daemon mode, allowing multiple clients to connect to a single, long-lived instance. You can enable this mode using the -gopls-remote flag, which will either connect to an existing shared gopls instance or create one if none is running. This can improve performance and reduce resource usage.
A number of additional options are provided to enable runtime logging and profiling tools.
-goplsLog string
The file to log gopls output, or leave empty to disable logging.
-goplsRPCTrace
Set gopls to log input and output messages.
-gopls-remote
Specify remote gopls instance to connect to.
-help
Print help and exit.
-http string
Enable http debug server by setting a listen address (e.g. localhost:7474)
-log string
The file to log templ LSP output to, or leave empty to disable logging.
-pprof
Enable pprof web server (default address is localhost:9999)
Ensuring templ files have been committed
It's common practice to commit generated *_templ.go files to your source code repository, so that your codebase is always in a state where it can be built and run without needing to run templ generate, e.g. by running go install on your project, or by importing it as a dependency in another project.
In your CI/CD pipeline, if you want to check that templ generate has been ran on all templ files (with the same version of templ used by the CI/CD pipeline), you can run templ generate again.
If any files have changed, then the pipeline should fail, as this would indicate that the generated files are not up-to-date with the templ files.
templ generate
git diff --exit-code
IDE support
Visual Studio Code
There's a VS Code extension, just make sure you've already installed templ and that it's on your path.
VSCodium users can find the extension on the Open VSX Registry at https://open-vsx.org/extension/a-h/templ
Format on Save
Include the following into your settings.json to activate formatting .templ files on save with the
templ plugin:
{
"editor.formatOnSave": true,
"[templ]": {
"editor.defaultFormatter": "a-h.templ"
},
}
Tailwind CSS Intellisense
Include the following to the settings.json in order to enable autocompletion for Tailwind CSS in .templ files:
{
"tailwindCSS.includeLanguages": {
"templ": "html"
}
}
:::note
Tailwind language servers require a tailwind.config.js file to be present in the root of your project. You can create a new config file with npx tailwindcss init, or use samples available at https://tailwindcss.com/docs/configuration
:::
Emmet HTML completion
Include the following to the settings.json in order to get smooth HTML completion via emmet (such as expanding input:button<Tab> to <input type="button" value="">). The emmet plugin is built into vscode and just needs to be activated for .templ files:
{
"emmet.includeLanguages": {
"templ": "html"
}
}
Neovim > 0.5.0
A plugin written in VimScript which adds syntax highlighting: joerdav/templ.vim.
For neovim you can use nvim-treesitter and install tree-sitter-templ with :TSInstall templ.
The configuration for the templ Language Server is included in lspconfig, mason, and mason-lspconfig.
The templ command must be in your system path for the LSP to be able to start. Ensure that you can run it from the command line before continuing.
Installing and configuring the templ LSP is no different to setting up any other Language Server.
local lspconfig = require("lspconfig")
-- Use a loop to conveniently call 'setup' on multiple servers and
-- map buffer local keybindings when the language server attaches
local servers = { 'gopls', 'ccls', 'cmake', 'tsserver', 'templ' }
for _, lsp in ipairs(servers) do
lspconfig[lsp].setup({
on_attach = on_attach,
capabilities = capabilities,
})
end
In Neovim, you can use the :LspInfo command to check which Language Servers (if any) are running. If the expected language server has not started, it could be due to the unregistered templ file extension.
To resolve this issue, add the following code to your configuration. This is also necessary for other LSPs to "pick up" on .templ files.
vim.filetype.add({ extension = { templ = "templ" } })
Other LSPs within .templ files
These LSPs can be used in conjunction with the templ lsp and tree-sitter. Here's how to set them up.
html-lsp - First make sure you have it installed :LspInstall html or find it on the :Mason list.
lspconfig.html.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "html", "templ" },
})
htmx-lsp - First make sure you have it installed :LspInstall htmx or find it on the :Mason list. Note with this LSP, it activates after you type hx- in an html attribute, because that's how all htmx attributes are written.
lspconfig.htmx.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "html", "templ" },
})
tailwindcss - First make sure you have it installed :LspInstall tailwindcss or find it on the :Mason list.
lspconfig.tailwindcss.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "templ", "astro", "javascript", "typescript", "react" },
settings = {
tailwindCSS = {
includeLanguages = {
templ = "html",
},
},
},
})
Inside of your tailwind.config.js, you need to tell tailwind to look inside of .templ files and/or .go files.
:::tip
If you don't have a tailwind.config.js in the root directory of your project, the Tailwind LSP won't activate, and you won't see autocompletion results.
:::
module.exports = {
content: [ "./**/*.html", "./**/*.templ", "./**/*.go", ],
theme: { extend: {}, },
plugins: [],
}
Formatting
With the templ LSP installed and configured, you can use the following code snippet to format on save:
vim.api.nvim_create_autocmd({ "BufWritePre" }, { pattern = { "*.templ" }, callback = vim.lsp.buf.format })
BufWritePre means that the callback gets ran after you call :write.
If you have multiple LSPs attached to the same buffer, and you have issues with vim.lsp.buf.format, you can use this snippet to run templ fmt in the same way that you might from the command line.
This will get the buffer and its corresponding filename, and refresh the buffer after it has been formatted so you don't get out of sync issues.
local custom_format = function()
if vim.bo.filetype == "templ" then
local bufnr = vim.api.nvim_get_current_buf()
local filename = vim.api.nvim_buf_get_name(bufnr)
local cmd = "templ fmt " .. vim.fn.shellescape(filename)
vim.fn.jobstart(cmd, {
on_exit = function()
-- Reload the buffer only if it's still the current buffer
if vim.api.nvim_get_current_buf() == bufnr then
vim.cmd('e!')
end
end,
})
else
vim.lsp.buf.format()
end
end
To apply this custom_format in your neovim configuration as a keybinding, apply it to the on_attach function.
local on_attach = function(client, bufnr)
local opts = { buffer = bufnr, remap = false }
-- other configuration options
vim.keymap.set("n", "<leader>lf", custom_format, opts)
end
To make this custom_format run on save, make the same autocmd from before and replace the callback with custom_format.
vim.api.nvim_create_autocmd({ "BufWritePre" }, { pattern = { "*.templ" }, callback = custom_format })
You can also rewrite the function like so, given that the function will only be executed on .templ files.
local templ_format = function()
local bufnr = vim.api.nvim_get_current_buf()
local filename = vim.api.nvim_buf_get_name(bufnr)
local cmd = "templ fmt " .. vim.fn.shellescape(filename)
vim.fn.jobstart(cmd, {
on_exit = function()
-- Reload the buffer only if it's still the current buffer
if vim.api.nvim_get_current_buf() == bufnr then
vim.cmd('e!')
end
end,
})
end
Troubleshooting
If you cannot run :TSInstall templ, ensure you have an up-to-date version of tree-sitter. The package for templ was added to the main tree-sitter repository so you shouldn't need to install a separate plugin for it.
If you still don't get syntax highlighting after it's installed, try running :TSBufEnable highlight. If you find that you need to do this every time you open a .templ file, you can run this autocmd to do it for your neovim configuration.
vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.templ", callback = function() vim.cmd("TSBufEnable highlight") end })
Minimal Config
Minimal config with the following features (useful for debugging):
- lazy-vim: neovim package manager
- lsp config
- templ-lsp
- html-lsp
- htmx-lsp
- tailwind-lsp
- cmp: for autocompletion
- tree-sitter: for synx highlighting
To use this config:
- As a permanent setup: Create/replace
init.luain your config folder (~/.config/nvim/) - As a temporary setup: create a new folder in your
.config(e.g.~/.config/nvim_test) and tell neovim to start up with that as the nvim appnameNVIM_APPNAME=nvim_test nvim(see neovim docs for further explanation.
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git",
"clone",
"--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", -- latest stable release
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
vim.g.mapleader = " " -- Make sure to set `mapleader` before lazy so your mappings are correct
require("lazy").setup({
'neovim/nvim-lspconfig',
{
-- Autocompletion
'hrsh7th/nvim-cmp',
dependencies = {
'hrsh7th/cmp-nvim-lsp',
},
},
{
-- Highlight, edit, and navigate code
'nvim-treesitter/nvim-treesitter',
dependencies = {
'vrischmann/tree-sitter-templ',
},
build = ':TSUpdate',
},
})
vim.filetype.add({ extension = { templ = "templ" } })
capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)
local lspconfig = require("lspconfig")
lspconfig.templ.setup{
on_attach = on_attach,
capabilities = capabilities,
}
lspconfig.tailwindcss.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "templ", "astro", "javascript", "typescript", "react" },
init_options = { userLanguages = { templ = "html" } },
})
lspconfig.html.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "html", "templ" },
})
lspconfig.htmx.setup({
on_attach = on_attach,
capabilities = capabilities,
filetypes = { "html", "templ" },
})
local cmp = require 'cmp'
cmp.setup({
mapping = cmp.mapping.preset.insert({
['<C-b>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(),
['<C-e>'] = cmp.mapping.abort(),
['<CR>'] = cmp.mapping.confirm({ select = true }),
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
})
})
require'nvim-treesitter.configs'.setup {
ensure_installed = { "templ" },
sync_install = false,
auto_install = true,
ignore_install = { "javascript" },
highlight = {
enable = true,
},
}
Vim
This requires Vim version 8 or later. Install LSP and autocomplete plugins, using vim-plug or other plugin manager.
Note: this example is for vim-lsp. Other LSP plugins can be also be used, but they need to be configured differently.
Plug 'prabirshrestha/vim-lsp'
Plug 'prabirshrestha/asyncomplete.vim'
Plug 'prabirshrestha/asyncomplete-lsp.vim'
Add configuration:
" Register LSP server for Templ.
au User lsp_setup call lsp#register_server({
\ 'name': 'templ',
\ 'cmd': [$GOPATH . '/bin/templ', 'lsp'],
\ 'allowlist': ['templ'],
\ })
function! s:on_lsp_buffer_enabled() abort
setlocal signcolumn=yes
if exists('+tagfunc') | setlocal tagfunc=lsp#tagfunc | endif
nmap <buffer> gd <plug>(lsp-definition)
nmap <buffer> gs <plug>(lsp-document-symbol-search)
nmap <buffer> gS <plug>(lsp-workspace-symbol-search)
nmap <buffer> gr <plug>(lsp-references)
nmap <buffer> gi <plug>(lsp-implementation)
nmap <buffer> gt <plug>(lsp-type-definition)
nmap <buffer> <leader>rn <plug>(lsp-rename)
nmap <buffer> [g <plug>(lsp-previous-diagnostic)
nmap <buffer> ]g <plug>(lsp-next-diagnostic)
nmap <buffer> K <plug>(lsp-hover)
let g:lsp_format_sync_timeout = 1000
autocmd! BufWritePre *.templ call execute('LspDocumentFormatSync')
endfunction
augroup lsp_install
au!
" call s:on_lsp_buffer_enabled only for languages that has the server registered.
autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
See vim-lsp for additional configuration options.
Configure autocomplete, for example:
inoremap <expr> <Tab> pumvisible() ? "\<C-n>" : "\<Tab>"
inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>"
inoremap <expr> <cr> pumvisible() ? asyncomplete#close_popup() : "\<cr>"
See asyncomplete.vim for more options.
If you're also using deoplete, you may need to disable it for templ files to
avoid conflict with asyncomplete:
autocmd FileType templ call deoplete#custom#buffer_option('auto_complete', v:false)
Optional: If you'd like indentation to better match Go outside of templ blocks, install:
Plug 'iefserge/templ.vim'
- This plugin also adds tcomment_vim support.
- This is a fork of joerdav/templ.vim.
Helix
Helix has built-in templ support in unstable since https://github.com/helix-editor/helix/pull/8540/commits/084628d3e0c29f4021f53b3e45997ae92033d2d2
It will be included in official releases after version 23.05.
Emacs
templ-ts-mode is a major mode for templ files that provides syntax highlighting, indentation, and the other usual major mode things. It is available on MELPA and can be installed like any other Emacs package.
Templ support requires the tree-sitter parser for Templ. If the parser is missing, the mode asks you on first use whether you want to download and build it via treesit-install-language-grammar (requires git and a C compiler).
Troubleshooting
Check that go, gopls and templ are installed and are present in the path
which go gopls templ
You should see 3 lines returned, showing the location of each binary:
/run/current-system/sw/bin/go
/Users/adrian/go/bin/gopls
/Users/adrian/bin/templ
Check that you can run the templ binary
Run templ lsp --help, you should see help text.
- If you can't run the
templcommand at the command line:- Check that the
templbinary is within a directory that's in your path (echo $PATHfor Linux/Mac/WSL,$env:pathfor Powershell). - Update your profile to ensure that the change to your path applies to new shells and processes.
- On MacOS / Linux, you may need to update your
~/.zsh_profile,~/.bash_profileor~/.profilefile. - On Windows, you will need to use the "Environment Variables" dialog. For WSL, use the Linux config.
- On MacOS / Linux, you may need to update your
- On MacOS / Linux, check that the file is executable and resolve it with
chmod +x /path/to/templ. - On MacOS, you might need to go through the steps at https://support.apple.com/en-gb/guide/mac-help/mh40616/mac to enable binaries from an "unidentified developer" to run.
- Check that the
- If you're running VS Code using Windows Subsystem for Linux (WSL), then templ must also be installed within the WSL environment, not just inside your Windows environment.
- If you're running VS Code in a Devcontainer, it must be installed in there.
Enable LSP logging
For VS Code, use the "Preferences: Open User Settings (JSON)" command in VS Code and add the configuration options.
{
// More settings...
"templ.log": "/Users/adrian/templ.log",
"templ.goplsLog": "/Users/adrian/gopls.log",
"templ.http": "localhost:7575",
"templ.goplsRPCTrace": true,
"templ.pprof": false,
// More stuff...
}
For Neovim, configure the LSP command to add the additional command line options.
local configs = require('lspconfig.configs')
configs.templ = {
default_config = {
cmd = { "templ", "lsp", "-http=localhost:7474", "-log=/Users/adrian/templ.log" },
filetypes = { 'templ' },
root_dir = nvim_lsp.util.root_pattern("go.mod", ".git"),
settings = {},
},
}
For IntelliJ, configure the plugin settings .idea/templ.xml.
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TemplSettings">
<option name="goplsLog" value="$USER_HOME$/gopls.log" />
<option name="goplsRPCTrace" value="true" />
<option name="http" value="localhost:7575" />
<option name="log" value="$USER_HOME$/templ.log" />
</component>
</project>
Make a minimal reproduction, and include the logs
The logs can be quite verbose, since almost every keypress results in additional logging. If you're thinking about submitting an issue, please try and make a minimal reproduction.
Look at the web server
The LSP has a http argument that starts a web server that can show the internal state of the LSP - in particular, the mapping between templ files and Go source code. The default is templ lsp -http=localhost:7474. See the log options above for instructions on how to set it.
Run templ info
The templ info command outputs information that's useful for debugging issues.
"missing metadata for import" / "could not import"
If you see an error like this coming from gopls:
could not import strconv (missing metadata for import of "strconv") compiler (BrokenImport)
Running go mod tidy in your project usually solves it.
Coding assistants / LLMs
To provide AI coding assistants such as GitHub Copilot, Cursor or similar with help on how to write templ code, the templ project maintains a single file containing documentation for LLMs to read.
You can find the file at https://templ.guide/llms.md.
LLM tools
https://github.com/CopilotC-Nvim/CopilotChat.nvim
CopilotChat is a plugin for Neovim that provides a chat interface for GitHub Copilot. It allows you to ask Copilot questions and get responses in real-time.
Use the URL feature to load https://templ.guide/llms.md.
Live reload
To enable live reload on a templ app use:
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
This will:
- Automatically re-generate Go code if you change
*.templfiles. - Restart the web server if you change
*.gofiles. - Automatically reload the browser if you change
*.goor*.templfiles. - Run a HTTP proxy on
localhost:7331(by default) that proxies requests to your web server (defaulthttp://localhost:8080).
Example
Create main.go and hello.templ files.
package main
import (
"fmt"
"net/http"
"github.com/a-h/templ"
)
func main() {
component := hello("World")
http.Handle("/", templ.Handler(component))
fmt.Println("Listening on :8080")
http.ListenAndServe(":8080", nil)
}
package main
templ hello(name string) {
<body>
<div>Hello, { name }</div>
</body>
}
Run templ generate --watch --proxy="http://localhost:8080" --cmd="go run .".
Observe that the web server is started, and the browser opens to http://localhost:7331.
Make changes to hello.templ and main.go, and see the changes reflected in the browser without having to press F5.
How it works
templ watches files for changes
The templ generate --watch argument tells templ to watch for changes to *.templ and *.go files in the current directory.
When a change is detected, templ will:
- Automatically generate
*.gocode from your*.templfiles when you save changes to them. - Create text files in the
tmpdirectory that is read by generated templ Go code if theTEMPL_DEV_MODEenabled.- This means that the Go web server doesn't need to be restarted when changes are made to HTML or text in
*.templfiles,templcan read the files at runtime instead. - The web server is only restarted when changes are made to Go code in
*.templfiles.
- This means that the Go web server doesn't need to be restarted when changes are made to HTML or text in
templ restarts your server automatically
The --cmd argument tells templ to run a command when *.go files change, for example:
templ generate --watch --cmd="go run ."
The command is executed if *.go files change, or if any Go code within *.templ files change.
You can run any command you like, e.g. go build -o app && ./app, or air, or wgo.
templ uses a proxy to auto-reload the browser
The --proxy argument tells templ to run a HTTP proxy that proxies requests to your web server.
For example, if your web server listens on port 8080:
templ generate --watch --cmd="go run ." --proxy="http://localhost:8080"
This starts a HTTP proxy that proxies requests to your web server (default http://localhost:7331). The proxy inserts client-side JavaScript before the </body> tag that will cause the browser to reload the window when the app is restarted instead of you having to reload the page manually - no more pressing F5!
By default, the proxy binds to 127.0.0.1:7331. You can use --proxybind to bind to another address, e.g., --proxybind="0.0.0.0".
:::note In order for templ to successfully inject the reload JavaScript into the HTML response:
- The HTML must have a
<body>tag. - The HTML must be served with a
Content-Typeoftext/html. - The response must be compressed with no compression, or a supported compression algorithm (e.g. gzip). :::
Live reload process
The live reload process can be shown in the following diagram:
sequenceDiagram
browser->>templ_proxy: HTTP
activate templ_proxy
templ_proxy->>app: HTTP
activate app
app->>templ_proxy: HTML
deactivate app
templ_proxy->>templ_proxy: add reload script
templ_proxy->>browser: HTML
deactivate templ_proxy
browser->>templ_proxy: SSE request to /_templ/reload/events
activate templ_proxy
templ_proxy->>generate: run templ generate if *.templ files have changed
templ_proxy->>app: restart app if *.go files have changed
templ_proxy->>browser: notify browser to reload page
deactivate templ_proxy
Triggering live reload from outside templ generate --watch
If you want to trigger a live reload from outside templ generate --watch (e.g. if you're using air, wgo or another tool to build, but you want to use the templ live reload proxy), you can use the --notify-proxy argument.
templ generate --notify-proxy
This will default to the default templ proxy address of localhost:7331, but can be changed with the --proxybind and --proxyport arguments.
templ generate --notify-proxy --proxybind="localhost" --proxyport="8080"
Alternatives
If you don't want to use templ generate --watch, you can use other tools to watch for changes and restart the server.
wgo
wgo:
Live reload for Go apps. Watch arbitrary files and respond with arbitrary commands. Supports running multiple invocations in parallel.
wgo -file=.go -file=.templ -xfile=_templ.go templ generate :: go run main.go
To avoid a continuous reloading files ending with _templ.go should be skipped via -xfile.
air
Air can also monitor the filesystem for changes, and provides a proxy to automatically reload pages.
It uses a toml configuration file.
See https://github.com/cosmtrek/air for details.
Example configuration
root = "."
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "templ generate && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor"]
exclude_file = []
exclude_regex = [".*_templ.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "templ", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
[proxy]
enabled = true
proxy_port = 8383
app_port = 8282
Live reload with other tools
Browser live reload allows you to see your changes immediately without having to switch to your browser and press F5 or CMD+R.
However, Web projects usually involve multiple build processes, e.g. css bundling, js bundling, alongside templ code generation and Go compilation.
Tools like air can be used with templ's built-in proxy server to carry out additional steps.
Example
This example, demonstrates setting up a live reload environment that integrates:
- Tailwind CSS for generating a css bundle.
- esbuild for bundling JavaScript or TypeScript.
- air for re-building Go source as well as sending a reload event to the
templproxy server.
How does it work
templ's built-in proxy server automatically refreshes the browser when a file changes. The proxy server injects a script that reloads the page in the browser if a "reload" event is sent to the browser by the proxy. See Live Reload page for a detailed explanation.
:::tip
The live reload JavaScript is only injected by the templ proxy if your HTML file contains a closing </body> tag.
:::
The "reload" event can be triggered in two ways:
templ generate --watchsends the event whenever a ".templ" file changes.- Manually trigger it by sending a HTTP POST request to
/_templ/reload/eventendpoint. ThetemplCLI provides this viatempl generate --notify-proxy.
:::tip
templ proxy server --watch mode generates different _templ.go files. In --watch mode _templ.txt files are generated that contain just the text that's in templ files. This is used to skip compilation of the Go code when only the text content changes.
:::
Setting up the Makefile
A Makefile can be used to run all of the necessary commands in parallel. This is useful for starting all of the watch processes at once.
templ watch mode
To start the templ proxy server in watch mode, run:
templ generate --watch --proxy="http://localhost:8080" --open-browser=false
This assumes that your http server is running on http://localhost:8080. --open-browser=false is to prevent templ from opening the browser automatically.
Tailwind CSS
Tailwind requires a tailwind.config.js file at the root of your project, alongside an input.css file.
npx --yes tailwindcss -i ./input.css -o ./assets/styles.css --minify --watch
This will watch input.css as well as your .templ files and re-generate assets/styles.css whenever there's a change.
esbuild
To bundle JavaScript, TypeScript, JSX, or TSX files, you can use esbuild:
npx --yes esbuild js/index.ts --bundle --outdir=assets/ --watch
This will watch js/index.ts and relevant files, and re-generate assets/index.js whenever there's a change.
Re-build Go source
To watch and restart your Go server, when only the go files change you can use air:
go run github.com/cosmtrek/air@v1.51.0 \
--build.cmd "go build -o tmp/bin/main" --build.bin "tmp/bin/main" --build.delay "100" \
--build.exclude_dir "node_modules" \
--build.include_ext "go" \
--build.stop_on_error "false" \
--misc.clean_on_exit true
:::tip
Using go run directly allows the version of air to be specified. This ensures that the version of air is consistent between machines. In addition, you don't need to run air init to generate .air.toml.
:::
:::note
This command doesn't do anything to restart or send a reload event to the templ proxy server. We'll use a separate air command to trigger a notify event when any non-go related files change.
:::
Reload event
We also want the browser to automatically reload when the:
- HTML content changes
- CSS bundle changes
- JavaScript bundle changes
To trigger the event, we can use the air command to use a different set of options, using the templ CLI to send a reload event to the browser.
go run github.com/cosmtrek/air@v1.51.0 \
--build.cmd "templ generate --notify-proxy" \
--build.bin "true" \
--build.delay "100" \
--build.exclude_dir "" \
--build.include_dir "assets" \
--build.include_ext "js,css"
:::note
The build.bin option is set to use the true command instead of executing the output of the build.cmd option, because the templ generate --notify-proxy command doesn't build anything, it just sends a reload event to the templ proxy server.
true is a command that exits with a zero status code, so you might see Process Exit with Code 0 printed to the console.
:::
Serving static assets
When using live reload, static assets must be served directly from the filesystem instead of being embedded in the Go binary, because the Go binary won't be re-built when the assets change.
In practice this means using http.Dir instead of http.FS to serve your assets.
If you don't want to do this, you can add additional asset file extensions to the --build.include_ext argument of the air command that rebuilds Go code to force a recompilation and restart of the Go server when the assets change.
Before
//go:embed assets/*
var assets embed.FS
...
mux.Handle("/assets/", http.FileServer(http.FS(assets)))
After
mux.Handle("/assets/",
http.StripPrefix("/assets",
http.FileServer(http.Dir("assets"))))
:::tip Web browsers will cache assets when they receive a HTTP 304 response. This will result in asset changes not being visible within your application.
To avoid this, set the Cache-Control header to no-store for assets in development mode:
var dev = true
func disableCacheInDevMode(next http.Handler) http.Handler {
if !dev {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
mux.Handle("/assets/",
disableCacheInDevMode(
http.StripPrefix("/assets",
http.FileServer(http.Dir("assets")))))
:::
Putting it all together
A Makefile can be used to run all of the commands in parallel.
# run templ generation in watch mode to detect all .templ files and
# re-create _templ.txt files on change, then send reload event to browser.
# Default url: http://localhost:7331
live/templ:
templ generate --watch --proxy="http://localhost:8080" --open-browser=false -v
# run air to detect any go file changes to re-build and re-run the server.
live/server:
go run github.com/cosmtrek/air@v1.51.0 \
--build.cmd "go build -o tmp/bin/main" --build.bin "tmp/bin/main" --build.delay "100" \
--build.exclude_dir "node_modules" \
--build.include_ext "go" \
--build.stop_on_error "false" \
--misc.clean_on_exit true
# run tailwindcss to generate the styles.css bundle in watch mode.
live/tailwind:
npx --yes tailwindcss -i ./input.css -o ./assets/styles.css --minify --watch
# run esbuild to generate the index.js bundle in watch mode.
live/esbuild:
npx --yes esbuild js/index.ts --bundle --outdir=assets/ --watch
# watch for any js or css change in the assets/ folder, then reload the browser via templ proxy.
live/sync_assets:
go run github.com/cosmtrek/air@v1.51.0 \
--build.cmd "templ generate --notify-proxy" \
--build.bin "true" \
--build.delay "100" \
--build.exclude_dir "" \
--build.include_dir "assets" \
--build.include_ext "js,css"
# start all 5 watch processes in parallel.
live:
make -j5 live/templ live/server live/tailwind live/esbuild live/sync_assets
:::note
The -j5 argument to make runs all 5 commands in parallel.
:::
Run make live to start all of the watch processes.
Components
templ Components are markup and code that is compiled into functions that return a templ.Component interface by running the templ generate command.
Components can contain templ elements that render HTML, text, expressions that output text or include other templates, and branching statements such as if and switch, and for loops.
package main
templ headerTemplate(name string) {
<header data-testid="headerTemplate">
<h1>{ name }</h1>
</header>
}
The generated code is a Go function that returns a templ.Component.
func headerTemplate(name string) templ.Component {
// Generated contents
}
templ.Component is an interface that has a Render method on it that is used to render the component to an io.Writer.
type Component interface {
Render(ctx context.Context, w io.Writer) error
}
:::tip Since templ produces Go code, you can share templates the same way that you share Go code - by sharing your Go module.
templ follows the same rules as Go. If a templ block starts with an uppercase letter, then it is public, otherwise, it is private.
A templ.Component may write partial output to the io.Writer if it returns an error. If you want to ensure you only get complete output or nothing, write to a buffer first and then write the buffer to an io.Writer.
:::
Code-only components
Since templ Components ultimately implement the templ.Component interface, any code that implements the interface can be used in place of a templ component generated from a *.templ file.
package main
import (
"context"
"io"
"os"
"github.com/a-h/templ"
)
func button(text string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<button>"+text+"</button>")
return err
})
}
func main() {
button("Click me").Render(context.Background(), os.Stdout)
}
<button>
Click me
</button>
:::warning
This code is unsafe! In code-only components, you're responsible for escaping the HTML content yourself, e.g. with the templ.EscapeString function.
:::
Method components
templ components can be returned from methods (functions attached to types).
Go code:
package main
import "os"
type Data struct {
message string
}
templ (d Data) Method() {
<div>{ d.message }</div>
}
func main() {
d := Data{
message: "You can implement methods on a type.",
}
d.Method().Render(context.Background(), os.Stdout)
}
It is also possible to initialize a struct and call its component method inline.
package main
import "os"
type Data struct {
message string
}
templ (d Data) Method() {
<div>{ d.message }</div>
}
templ Message() {
<div>
@Data{
message: "You can implement methods on a type.",
}.Method()
</div>
}
func main() {
Message().Render(context.Background(), os.Stdout)
}
View models
With templ, you can pass any Go type into your template as parameters, and you can call arbitrary functions.
However, if the parameters of your template don't closely map to what you're displaying to users, you may find yourself calling a lot of functions within your templ files to reshape or adjust data, or to carry out complex repeated string interpolation or URL constructions.
This can make template rendering hard to test, because you need to set up complex data structures in the right way in order to render the HTML. If the template calls APIs or accesses databases from within the templates, it's even harder to test, because then testing your templates becomes an integration test.
A more reliable approach can be to create a "View model" that only contains the fields that you intend to display, and where the data structure closely matches the structure of the visual layout.
package invitesget
type Handler struct {
Invites *InviteService
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
invites, err := h.Invites.Get(getUserIDFromContext(r.Context()))
if err != nil {
//TODO: Log error server side.
}
m := NewInviteComponentViewModel(invites, err)
teamInviteComponent(m).Render(r.Context(), w)
}
func NewInviteComponentViewModel(invites []models.Invite, err error) (m InviteComponentViewModel) {
m.InviteCount = len(invites)
if err != nil {
m.ErrorMessage = "Failed to load invites, please try again"
}
return m
}
type InviteComponentViewModel struct {
InviteCount int
ErrorMessage string
}
templ teamInviteComponent(model InviteComponentViewModel) {
if model.InviteCount > 0 {
<div>You have { fmt.Sprintf("%d", model.InviteCount) } pending invites</div>
}
if model.ErrorMessage != "" {
<div class="error">{ model.ErrorMessage }</div>
}
}
Testing
To test that data is rendered as expected, there are two main ways to do it:
- Expectation testing - testing that specific expectations are met by the output.
- Snapshot testing - testing that outputs match a pre-written output.
Expectation testing
Expectation testing validates that the right data appears in the output in the right format and position.
The example at https://github.com/a-h/templ/blob/main/examples/blog/posts_test.go shows how to test that a list of posts is rendered correctly.
These tests use the goquery library to parse HTML and check that expected elements are present. goquery is a jQuery-like library for Go, that is useful for parsing and querying HTML. You’ll need to run go get github.com/PuerkitoBio/goquery to add it to your go.mod file.
Testing components
The test sets up a pipe to write templ's HTML output to, and reads the output from the pipe, parsing it with goquery.
First, we test the page header. To use goquery to inspect the output, we’ll need to connect the header component’s Render method to the goquery.NewDocumentFromReader function with an io.Pipe.
func TestHeader(t *testing.T) {
// Pipe the rendered template into goquery.
r, w := io.Pipe()
go func () {
_ = headerTemplate("Posts").Render(context.Background(), w)
_ = w.Close()
}()
doc, err := goquery.NewDocumentFromReader(r)
if err != nil {
t.Fatalf("failed to read template: %v", err)
}
// Expect the component to be present.
if doc.Find(`[data-testid="headerTemplate"]`).Length() == 0 {
t.Error("expected data-testid attribute to be rendered, but it wasn't")
}
// Expect the page name to be set correctly.
expectedPageName := "Posts"
if actualPageName := doc.Find("h1").Text(); actualPageName != expectedPageName {
t.Errorf("expected page name %q, got %q", expectedPageName, actualPageName)
}
}
The header template (the "subject under test") includes a placeholder for the page name, and a data-testid attribute that makes it easier to locate the headerTemplate within the HTML using a CSS selector of [data-testid="headerTemplate"].
templ headerTemplate(name string) {
<header data-testid="headerTemplate">
<h1>{ name }</h1>
</header>
}
We can also test that the navigation bar was rendered.
func TestNav(t *testing.T) {
r, w := io.Pipe()
go func() {
_ = navTemplate().Render(context.Background(), w)
_ = w.Close()
}()
doc, err := goquery.NewDocumentFromReader(r)
if err != nil {
t.Fatalf("failed to read template: %v", err)
}
// Expect the component to include a testid.
if doc.Find(`[data-testid="navTemplate"]`).Length() == 0 {
t.Error("expected data-testid attribute to be rendered, but it wasn't")
}
}
Testing that it was rendered is useful, but it's even better to test that the navigation includes the correct nav items.
In this test, we find all of the a elements within the nav element, and check that they match the expected items.
navItems := []string{"Home", "Posts"}
doc.Find("nav a").Each(func(i int, s *goquery.Selection) {
expected := navItems[i]
if actual := s.Text(); actual != expected {
t.Errorf("expected nav item %q, got %q", expected, actual)
}
})
To test the posts, we can use the same approach. We test that the posts are rendered correctly, and that the expected data is present.
Testing whole pages
Next, we may want to go a level higher and test the entire page.
Pages are also templ components, so the tests are structured in the same way.
There’s no need to test for the specifics about what gets rendered in the navTemplate or homeTemplate at the page level, because they’re already covered in other tests.
Some developers prefer to only test the external facing part of their code (e.g. at a page level), rather than testing each individual component, on the basis that it’s slower to make changes if the implementation is too tightly controlled.
For example, if a component is reused across pages, then it makes sense to test that in detail in its own test. In the pages or higher-order components that use it, there’s no point testing it again at that level, so we only check that it was rendered to the output by looking for its data-testid attribute, unless we also need to check what we're passing to it.
Testing the HTTP handler
Finally, we want to test the posts HTTP handler. This requires a different approach.
We can use the httptest package to create a test server, and use the net/http package to make a request to the server and check the response.
The tests configure the GetPosts function on the PostsHandler with a mock that returns a "database error", while the other returns a list of two posts. Here's what the PostsHandler looks like:
type PostsHandler struct {
Log *log.Logger
GetPosts func() ([]Post, error)
}
In the error case, the test asserts that the error message was displayed, while in the success case, it checks that the postsTemplate is present. It does not check that the posts have actually been rendered properly or that specific fields are visible, because that’s already tested at the component level.
Testing it again here would make the code resistant to refactoring and rework, but then again, we might have missed actually passing the posts we got back from the database to the posts template, so it’s a matter of risk appetite vs refactor resistance.
Note the switch to the table-driven testing format, a popular approach in Go for testing multiple scenarios with the same test code.
func TestPostsHandler(t *testing.T) {
tests := []struct {
name string
postGetter func() (posts []Post, err error)
expectedStatus int
assert func(doc *goquery.Document)
}{
{
name: "database errors result in a 500 error",
postGetter: func() (posts []Post, err error) {
return nil, errors.New("database error")
},
expectedStatus: http.StatusInternalServerError,
assert: func(doc *goquery.Document) {
expected := "failed to retrieve posts\n"
if actual := doc.Text(); actual != expected {
t.Errorf("expected error message %q, got %q", expected, actual)
}
},
},
{
name: "database success renders the posts",
postGetter: func() (posts []Post, err error) {
return []Post{
{Name: "Name1", Author: "Author1"},
{Name: "Name2", Author: "Author2"},
}, nil
},
expectedStatus: http.StatusInternalServerError,
assert: func(doc *goquery.Document) {
if doc.Find(`[data-testid="postsTemplate"]`).Length() == 0 {
t.Error("expected posts to be rendered, but it wasn't")
}
},
},
}
for _, test := range tests {
// Arrange.
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/posts", nil)
ph := NewPostsHandler()
ph.Log = log.New(io.Discard, "", 0) // Suppress logging.
ph.GetPosts = test.postGetter
// Act.
ph.ServeHTTP(w, r)
doc, err := goquery.NewDocumentFromReader(w.Result().Body)
if err != nil {
t.Fatalf("failed to read template: %v", err)
}
// Assert.
test.assert(doc)
}
}
Summary
- goquery can be used effectively with templ for writing component level tests.
- Adding
data-testidattributes to your code simplifies the test expressions you need to write to find elements within the output and makes your tests less brittle. - Testing can be split between the two concerns of template rendering, and HTTP handlers.
Snapshot testing
Snapshot testing is a more broad check. It simply checks that the output hasn't changed since the last time you took a copy of the output.
It relies on manually checking the output to make sure it's correct, and then "locking it in" by using the snapshot.
templ uses this strategy to check for regressions in behaviour between releases, as per https://github.com/a-h/templ/blob/main/generator/test-html-comment/render_test.go
To make it easier to compare the output against the expected HTML, templ uses a HTML formatting library before executing the diff.
package testcomment
import (
_ "embed"
"testing"
"github.com/a-h/templ/generator/htmldiff"
)
//go:embed expected.html
var expected string
func Test(t *testing.T) {
component := render("sample content")
diff, err := htmldiff.Diff(component, expected)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Error(diff)
}
}
Template generation
To generate Go code from *.templ files, use the templ command line tool.
templ generate
The templ generate recurses into subdirectories and generates Go code for each *.templ file it finds.
The command outputs warnings, and a summary of updates.
(!) void element <input> should not have child content [ from=12:2 to=12:7 ]
(✓) Complete [ updates=62 duration=144.677334ms ]
Advanced options
The templ generate command has a --help option that prints advanced options.
These include the ability to generate code for a single file and to choose the number of parallel workers that templ generate uses to create Go files.
By default templ generate uses the number of CPUs that your machine has installed.
templ generate --help
usage: templ generate [<args>...]
Generates Go code from templ files.
Args:
-path <path>
Generates code for all files in path. (default .)
-f <file>
Optionally generates code for a single file, e.g. -f header.templ
-stdout
Prints to stdout instead of writing generated files to the filesystem.
Only applicable when -f is used.
-source-map-visualisations
Set to true to generate HTML files to visualise the templ code and its corresponding Go code.
-include-version
Set to false to skip inclusion of the templ version in the generated code. (default true)
-include-timestamp
Set to true to include the current time in the generated code.
-watch
Set to true to watch the path for changes and regenerate code.
-cmd <cmd>
Set the command to run after generating code.
-proxy
Set the URL to proxy after generating code and executing the command.
-proxyport
The port the proxy will listen on. (default 7331)
-proxybind
The address the proxy will listen on. (default 127.0.0.1)
-notify-proxy
If present, the command will issue a reload event to the proxy 127.0.0.1:7331, or use proxyport and proxybind to specify a different address.
-w
Number of workers to use when generating code. (default runtime.NumCPUs)
-lazy
Only generate .go files if the source .templ file is newer.
-pprof
Port to run the pprof server on.
-keep-orphaned-files
Keeps orphaned generated templ files. (default false)
-v
Set log verbosity level to "debug". (default "info")
-log-level
Set log verbosity level. (default "info", options: "debug", "info", "warn", "error")
-help
Print help and exit.
Examples:
Generate code for all files in the current directory and subdirectories:
templ generate
Generate code for a single file:
templ generate -f header.templ
Watch the current directory and subdirectories for changes and regenerate code:
templ generate -watch
:::ti
Truncated - read the full file at https://github.com/CoreyCole/datastarui/blob/feb9af0c58ade31f8fefc1443b7e7e15ad413242/.cursor/rules/templ.mdc.