Views

SEO And Sitemaps

Configure SEO defaults, page metadata, robots.txt, and sitemap.xml for public GoLazy apps.

By Guillermo Alvarez - Published Updated

Keep SEO opt-in

Generated GoLazy apps do not need SEO defaults, a control plane, or a sitemap to serve their first page. Add SEO when the app has public pages that should be indexed, shared, or represented with stable metadata.

lazyapp registers the {{seo}} and {{seo_lang}} helpers automatically. Application-wide defaults are optional and come from lazyapp.Config.SEO.

Configure defaults in init/seo.go

lazyapp.Config.SEO is a function:

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

Define SEO in init/seo.go:

package appinit

import (
    "context"

    "golazy.dev/lazyseo"
)

func SEO(ctx context.Context) []lazyseo.Option {
    return []lazyseo.Option{
        lazyseo.SiteName("Example"),
        lazyseo.Description("An example GoLazy application."),
        lazyseo.Language("en"),
        lazyseo.Locale("en_US"),
        lazyseo.Type("website"),
        lazyseo.TwitterCardType("summary"),
    }
}

lazyapp.New runs Dependencies before it calls SEO, so this function can read services or settings that were placed into the app context during dependency initialization.

Render metadata in the layout

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

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

When no defaults or request metadata are set, the helpers render empty values.

Set page metadata

Controllers set request-local metadata:

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 an application/ld+json script in the head.

Generate sitemaps

Sitemap generation is opt-in. With no sitemap config, GoLazy does not serve /sitemap.xml and robots.txt does not advertise one.

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,
        Dependencies: Dependencies,
        SEO:          SEO,
        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
        }),
    },
},

When sitemap generation is enabled, the default robots.txt automatically adds the generated sitemap URL.

Configure robots

lazyapp serves permissive robots.txt by default:

User-agent: *
Allow: /

Configure robots explicitly when needed:

Robots: lazyapp.RobotsConfig{
    Rules: []lazyapp.RobotsRule{{
        UserAgent: "*",
        Disallow: []string{"/admin"},
    }},
},

Set Robots.Disabled or Sitemap.Disabled when the generated file should not be served. The newest sitemap LastUpdated value is also used as the generated Last-Modified header for cache validation.