Skip to content

The basics

Views

Server-rendered HTML with html/template, layouts, partials, fragments for htmx, request-aware helpers and optional templ support.

Last updated:

Views use Go's html/template, which escapes HTML, attributes, JavaScript and URLs by context. Enable the engine with vukem.WithViews(fsys):

bootstrap/app.go
vukem.WithViews(resources.Views()) // disk in development, embed.FS in the binary

Outside 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#

Text
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#

resources/views/layouts/app.html
<!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>
resources/views/posts/show.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:

HTML
{{/* layout: admin */}}
{{/* layout: none */}}

An unknown layout name is a boot error.

Rendering#

Go
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#

FunctionOutput
route "posts.show" "post" .IDURL of a named route
asset "css/app.css"/css/app.css (prefix configurable)
csrfField, csrfTokenCSRF hidden input or token (CSRF)
cspNonceThe 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", errorsFirst 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", viteReactRefreshVite dev server or manifest tags
t "key" "name" v, localeTranslations 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#

Go
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.