Core Framework

Views and Layouts

Render embedded html/template views through layouts with escaped data and explicit trusted HTML.

By Guillermo Alvarez - Published - Updated

Embed views

The application embeds its templates:

//go:embed views public
var Files embed.FS

app.Views returns the views subtree as an fs.FS. Startup passes that filesystem to lazyapp.New, which initializes the renderer.

The renderer requires a default layout:

layouts/app.html.tpl

Missing embedded resources fail during application initialization rather than on the first production request.

Name views by controller and action

Controller view paths follow:

app/views/<controller>/<action>.html.tpl

Given a request routed to PostsController#Index, an action can set view data and return nil:

c.Set("title", "Posts")
c.Set("posts", c.posts.List())
return nil

GoLazy then renders:

posts/index.html.tpl

Use Render("other") only when the action should render a non-default view.

Set template data

Add values before rendering:

c.Set("title", "Posts")
c.Set("posts", c.posts.List())

Templates access them by name:

<h1>{{ .title }}</h1>

{{ range .posts }}
  <a href="/posts/{{ .Slug }}">{{ .Title }}</a>
{{ end }}

The same data map is available to the layout.

Compose the layout

GoLazy executes the controller view first. It then passes the generated content to the selected layout as .content:

<!doctype html>
<html>
  <head>
    <title>{{ .title }}</title>
  </head>
  <body>
    <main>{{ .content }}</main>
  </body>
</html>

Use SetLayout before Render to select a layout other than app.

Register helpers

Application helpers are plain Go functions. The sample app keeps them in app/helpers and returns them as a helper map:

func RegisterHelpers() map[string]any {
    return map[string]any{
        "word_count": WordCount,
        "read_time":  ReadTime,
    }
}

Startup passes that map to lazyapp.New:

return lazyapp.New(lazyapp.Config{
    Name:    "sample_app",
    Drawer:  Draw,
    Public:  app.Public,
    Views:   app.Views,
    Context: Context,
    Helpers: lazyapp.Helpers{helpers.RegisterHelpers()},
})

Templates call registered helpers by name:

{{ word_count .post.Body }} words
{{ read_time .post.Body }} min read

Router helpers such as path_for are registered automatically by lazyapp. Asset helpers are registered automatically when public files or generated asset sources are configured.

lazyapp.New registers router, asset, and application helpers before caching templates. Treat the helper set as startup configuration. If you intentionally change helpers after setup in a custom assembly, call Views.Cache again before serving requests.

lazyapp registers public files with lazyassets and exposes helpers to templates:

{{stylesheet "/styles.css"}}

stylesheet renders a trusted stylesheet link whose href uses asset_path. asset_path returns the content-hashed permanent URL, such as /styles-<hash>.css. The original logical path still works, but the permanent URL can receive an immutable cache policy because the URL changes when the file content changes.

Use asset_integrity when a tag needs the matching SHA-256 integrity value:

<link
  rel="stylesheet"
  href="{{asset_path "/styles.css"}}"
  integrity="{{asset_integrity "/styles.css"}}">

permalink is kept as a compatibility alias for asset_path.

Escaping and trusted HTML

Go's html/template escapes ordinary data according to its HTML context. Keep user and application data as strings whenever possible.

Only convert HTML that was produced by a trusted renderer:

body, err := markdown.Convert(post.Body)
if err != nil {
    return fmt.Errorf("render post markdown: %w", err)
}

c.Set("body", template.HTML(body))

template.HTML disables escaping. Never use it merely to make untrusted input render as markup.

Rendering errors

Render returns contextual errors for:

  • Missing view files.
  • Invalid view templates.
  • View execution failures.
  • Missing or invalid layouts.
  • Layout execution failures.
  • Response write failures.

Return those errors from the action so the framework can produce the HTTP response.