Views use Go's html/template, which escapes HTML, attributes, JavaScript and URLs by context. Enable the engine with vukem.WithViews(fsys):
vukem.WithViews(resources.Views()) // disk in development, embed.FS in the binaryOutside production every render re-parses the views, so edits show up on refresh. Syntax errors in any view fail vukem.New at boot.
Directory layout#
resources/views/
├── layouts/app.html the default layout
├── partials/post-fields.html shared: {{template "partials/post-fields" .}}
├── components/alert.html shared, by convention for reusable widgets
├── home.html page "home"
└── posts/index.html page "posts/index"layouts/, partials/ and components/ are shared by every page. Any other .html file is a page named by its path without the extension. Each page is parsed into its own template set, so block names never collide between pages.
Layouts and blocks#
<!doctype html>
<html lang="en">
<head>
<title>{{block "title" .}}Vukem{{end}}</title>
<link rel="stylesheet" href="{{asset "css/app.css"}}">
{{htmxScript (asset "js/htmx.min.js")}}
</head>
<body>
{{with flash "success"}}<p class="flash">{{.}}</p>{{end}}
{{block "content" .}}{{end}}
</body>
</html>{{define "title"}}{{.Title}}{{end}}
{{define "content"}}
<h1>{{.Title}}</h1>
<p>{{.Body}}</p>
{{end}}Pages render inside layouts/app.html. A page picks another layout, or none, with a comment on its first line:
{{/* layout: admin */}}
{{/* layout: none */}}An unknown layout name is a boot error.
Rendering#
return c.Render("posts/show", post) // 200, full page
return c.Render("posts/index", data, web.Block("post-list")) // block for htmx partial requests
return c.Fragment("posts/index", "post-list", data) // always just the block
return c.Render("posts/create", nil, web.WithStatus(http.StatusUnprocessableEntity))Rendering is buffered: a template error never leaves a half-written page, it goes to the error handler instead. web.Block adds Vary: HX-Request. See HTMX.
Template functions#
| Function | Output |
|---|---|
route "posts.show" "post" .ID | URL of a named route |
asset "css/app.css" | /css/app.css (prefix configurable) |
csrfField, csrfToken | CSRF hidden input or token (CSRF) |
cspNonce | The request's CSP nonce, for inline <script nonce> / <style nonce> |
htmxScript "/js/htmx.min.js" | <script src defer nonce> with the request's nonce |
old "title" | Previous input after a failed validation (dotted paths for nested fields) |
error "title", errors | First error of a field, or all field errors |
flash "success" | A flashed session value |
json . | Value encoded as JSON, safe inside <script> |
dict "Key" v ... | A map, to pass several values to a partial |
vite "resources/js/app.ts", viteReactRefresh | Vite dev server or manifest tags |
t "key" "name" v, locale | Translations and the request's locale |
Request-scoped functions read the session and nonce from the request context; without a session they return empty values. Add your own with view.Config.Funcs when building a view.Engine yourself.
can checks authorization.
The engine directly#
engine, err := view.New(view.Config{
FS: os.DirFS("resources/views"),
Reload: true,
URLs: app.Router, // for the route function
AssetPrefix: "/static",
Funcs: template.FuncMap{"upper": strings.ToUpper},
})*view.Engine implements web.Renderer (Render, RenderBlock, Has) and lists pages with Names().
Error pages#
The error handler renders errors/<status> (for example resources/views/errors/404.html) when that view exists, and a minimal built-in page otherwise.
templ#
templ components satisfy web.View (Render(ctx, w) error), so c.View(components.PostList(posts)) renders them. templ is supported, not bundled.