Home / Docs / Templates and Head

Templates and Head

Guides

Forge handles the HTML <head> for you. Declare assets and SEO metadata once in main.go — every page gets them automatically.

forge:head

Every module template (list.html, show.html) has access to the forge:head named template. Place it inside your <head> element:

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  {{template "forge:head" .}}
</head>

forge:head renders: title, description meta, canonical link, Open Graph tags, Twitter Card tags, JSON-LD (app-level and per-item), and a robots noindex tag when Head.NoIndex is true.

HeadAssets

Declare favicons, stylesheets, fonts, and scripts once via app.SEO:

app.SEO(&forge.HeadAssets{
    Preconnect:  []string{"https://fonts.bunny.net"},
    Stylesheets: []string{
        "https://fonts.bunny.net/css?family=jetbrains-mono:400",
        "/static/css/app.css",
    },
    Favicons: []forge.FaviconLink{
        {Rel: "icon", Type: "image/x-icon", Href: "/static/favicon.ico"},
        {Rel: "icon", Type: "image/png", Sizes: "32x32", Href: "/static/favicon-32x32.png"},
        {Rel: "apple-touch-icon", Sizes: "180x180", Href: "/static/apple-touch-icon.png"},
    },
    Scripts: []forge.ScriptTag{
        {Src: "/static/app.js", Defer: true},
    },
})

forge:head injects them in the correct order: preconnect → stylesheets → favicons → scripts. Sites that do not call app.SEO(&HeadAssets{...}) are unaffected.

ScriptTag.Body is typed as template.JS for safe inline script emission:

forge.ScriptTag{Body: template.JS("window.analytics=window.analytics||[]")}

OGDefaults and AppSchema

Set app-level Open Graph fallbacks and JSON-LD once:

app.SEO(
    &forge.OGDefaults{
        Image:       forge.Image{URL: "https://example.com/og.png", Width: 1200, Height: 630},
        TwitterSite: "@yourhandle",
    },
    &forge.AppSchema{
        Type: "Organization",
        Name: "Your Site",
        URL:  "https://example.com",
    },
)

These are injected into every page via forge:head. Per-item metadata (from Head()) takes priority when present.

PageHead — custom handlers

Module templates receive TemplateData[T] which already carries all head fields. Custom handlers (like a home page) use forge.PageHead to get the same forge:head support:

type homeData struct {
    forge.PageHead   // embeds Head, OGDefaults, AppSchema, HeadAssets
    Posts []*Post
}

Populate PageHead in your handler:

data := homeData{
    PageHead: forge.PageHead{
        Head: forge.Head{
            Title:       "My Site — Home",
            Description: "Welcome to my site.",
            Canonical:   baseURL + "/",
        },
    },
    Posts: recentPosts,
}

OGDefaults, AppSchema, and HeadAssets are injected automatically by the framework — you only set Head explicitly.

ContextFunc — extra data in module templates

Module templates only receive their own content item by default. Use ContextFunc to supply additional data — for example, a full list of docs for a sidebar:

forge.ContextFunc(func(ctx forge.Context, _ any) (any, error) {
    return docRepo.FindAll(ctx, forge.ListOptions{
        Status: []forge.Status{forge.Published},
    })
})

The return value is available in templates as .Extra:

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

ContextFunc errors are logged by the framework and .Extra is set to nil — the render is never aborted. Pass _ any as the second argument when you do not need the current item.