Views

Template Data And Helpers

Pass controller data to templates and use registered helper functions.

By Guillermo Alvarez - Published - Updated

Set template data

Controllers pass data to templates with Set:

func (c *PostsController) Show(
    _ http.ResponseWriter,
    r *http.Request,
) error {
    post, ok := c.posts.Get(r.PathValue("post_id"))
    if !ok {
        return lazycontroller.Error(http.StatusNotFound, fmt.Errorf("post not found"))
    }

    c.Set("title", post.Title)
    c.Set("post", post)
    return nil
}

The view reads those values by name:

<h1>{{.title}}</h1>
<article>{{.post.Body}}</article>

Defer expensive values

Use SetLater when a value is expensive and likely to be needed by the view:

comments := c.SetLater("comments", func() ([]Comment, error) {
    return c.comments.ForPost(post.ID)
})

SetLater stores a lazycontroller.Valuer under the template key and starts the loader immediately. The controller can still call comments.Value() when it needs the result directly.

Use SetWhenNeeded when only some templates or partials will read the value:

c.SetWhenNeeded("related", func(ctx context.Context) ([]Post, error) {
    return c.posts.Related(ctx, post.ID)
})

Templates read both forms by calling Value:

{{range .comments.Value}}
  <p>{{.Body}}</p>
{{end}}

The loader runs once and the result or error is memoized for the request. If the loader returns an error, template rendering fails with that error.

Use built-in helpers

lazyapp registers route, asset, form, cache, and Turbo helpers:

<a href="{{path_for "posts"}}">Posts</a>
{{link_to "Posts" (path_for "posts") (attr "class" "nav-link")}}
{{link_to .post.Title (path_for "post" .post.Param) (data "turbo-frame" "post")}}
{{link_to "Posts" (path_for "posts") (unless_current)}}
{{stylesheet "/styles.css"}}
{{importmap "/assets/importmap.json"}}
{{ cache "featured" "post_card" .post }}

Helpers return ordinary strings or trusted fragments, depending on what the helper renders.

Use link_to when a template should render a complete anchor from a generated route path. attr adds a normal HTML attribute, data adds a data-* attribute, and unless_current renders only the escaped link text when the current request points at the same destination.

Register app helpers

Application helpers are passed to lazyapp.New:

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

Wire them during startup:

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

Then call the helper from a template:

<p>{{read_time .post.Body}} min read</p>

Add SEO metadata

lazyapp installs SEO helpers automatically. Configure application defaults with a context-aware SEO function, normally in init/seo.go, and wire it through lazyapp.Config.SEO:

func App() *lazyapp.App {
    return lazyapp.New(lazyapp.Config{
        Name:         "sample_app",
        Drawer:       Draw,
        Public:       app.Public,
        Views:        app.Views,
        Dependencies: Dependencies,
        SEO:          SEO,
    })
}
func SEO(ctx context.Context) []lazyseo.Option {
    return []lazyseo.Option{
        lazyseo.SiteName("GoLazy"),
        lazyseo.Description("A GoLazy application."),
        lazyseo.Language("en"),
        lazyseo.TwitterCardType("summary"),
    }
}

lazyapp.New calls Dependencies before SEO, so SEO defaults can read dependency-backed values from the app context.

Call {{seo_lang}} from the opening <html> tag and {{seo}} from the layout <head>:

<html lang="{{seo_lang}}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {{seo}}
  </head>
</html>

Read SEO And Sitemaps for controller metadata, structured data, robots.txt, and sitemap generation.

Use view variants

View variants are server-side template variants for the same route and format. They use the Rails-style +variant segment after the format:

app/views/posts/show.svg.tpl
app/views/posts/show.svg+square.tpl

Rendering show as svg with variant square tries the square template first and falls back to show.svg.tpl. Use this for generated image shapes such as a default social image and a square social image. These are not SEO alternates; SEO alternates are public URLs declared with c.Alternate(...).

Controllers can render that SVG content as a string:

svg, err := c.RenderSVGString("show", "square")
if err != nil {
    return err
}

That gives image-generation code a normal Go string to write, rasterize, or register as an asset before setting the public URL with c.SEOImage(...).