Views
Template Data And Helpers
Pass controller data to templates and use registered helper functions.
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>
Use built-in helpers
lazyapp registers route, asset, form, and Turbo helpers:
<a href="{{path_for "posts"}}">Posts</a>
{{stylesheet "/styles.css"}}
{{importmap "/assets/importmap.json"}}
Helpers return ordinary strings or trusted fragments, depending on what the helper renders.
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,
Context: Context,
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 in
lazyapp.Config.SEO:
func App() *lazyapp.App {
return lazyapp.New(lazyapp.Config{
Name: "sample_app",
SEO: []lazyseo.Option{
lazyseo.SiteName("GoLazy"),
lazyseo.Description("A GoLazy application."),
lazyseo.Language("en"),
lazyseo.TwitterCardType("summary"),
},
})
}
Application helpers should only register app-owned helpers:
func RegisterHelpers() map[string]any {
return map[string]any{
"read_time": ReadTime,
}
}
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>
Set per-action metadata from controllers:
func (c *PostsController) Show(postID string) error {
post, ok := c.posts.Get(postID)
if !ok {
return lazycontroller.Error(http.StatusNotFound, fmt.Errorf("post not found"))
}
c.Metadata(post)
c.Alternate("de", "https://example.com/de/posts/" + post.Param)
c.Set("post", post)
return nil
}
Metadata reads small optional interfaces from the model. The minimum useful
interface is Title() string; common optional interfaces include
Description() string, Canonical() string, Image() string,
ImageAlt() string, Kind() lazyseo.PageKind,
PublishedTime() time.Time, LastUpdated() time.Time,
OpenGraph() lazyseo.OpenGraph, TwitterCard() lazyseo.TwitterCard, and
JSONLD() any. If the model has enough information and does not provide
explicit JSON-LD, GoLazy emits a conventional JSON-LD node automatically.
Use controller helpers to make page-specific edits after Metadata:
c.OpenGraph(lazyseo.OpenGraph{
Description: "Social preview description",
Image: "/posts/hello-og.png",
ImageAlt: "Hello post social preview",
ImageWidth: 1200,
ImageHeight: 630,
})
c.TwitterCard(lazyseo.TwitterCard{
Card: "summary_large_image",
Image: "/posts/hello-twitter.png",
ImageAlt: "Hello post social preview",
})
Kind accepts shared page-kind constants such as lazyseo.Article,
lazyseo.WebPage, lazyseo.WebSite, lazyseo.Product, and
lazyseo.Restaurant; the same value carries the Open Graph type and the
schema.org type name for automatic JSON-LD work. PublishedTime renders
article:published_time, and LastUpdated renders article:modified_time.
Canonical renders
<link rel="canonical">. Alternate renders
<link rel="alternate" hreflang="...">. JSONLD renders a
application/ld+json script in the head. Use the golazy.dev/lazyseo/jsonld
subpackage for explicit common schema.org values such as jsonld.NewArticle,
jsonld.NewWebPage, jsonld.NewWebSite, jsonld.NewOrganization, and
jsonld.NewBreadcrumbList.
Generate robots and sitemaps
lazyapp serves /robots.txt with permissive defaults. Add sitemap entries in
application startup:
func App() *lazyapp.App {
return lazyapp.New(lazyapp.Config{
Name: "sample_app",
Drawer: Draw,
Public: app.Public,
Views: app.Views,
Context: Context,
Sitemap: lazyapp.SitemapConfig{
BaseURL: "https://example.com",
URLs: []lazyapp.SitemapURL{
{
Location: "/",
LastUpdated: time.Date(2026, 6, 20, 0, 0, 0, 0, time.UTC),
ChangeFreq: "weekly",
Priority: 1,
},
},
},
})
}
Use SitemapSourceFunc when URLs come from a service:
Sitemap: lazyapp.SitemapConfig{
BaseURL: "https://example.com",
Sources: []lazyapp.SitemapSource{
lazyapp.SitemapSourceFunc(func() ([]lazyapp.SitemapURL, error) {
return postSitemapURLs(), nil
}),
},
},
Configure or disable robots explicitly when needed:
Robots: lazyapp.RobotsConfig{
Rules: []lazyapp.RobotsRule{{
UserAgent: "*",
Disallow: []string{"/admin"},
}},
},
Set Robots.Disabled or Sitemap.Disabled to stop serving the generated file.
The newest sitemap LastUpdated value is also used as the generated
Last-Modified header for cache validation.
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(...).