Templates and Head
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.