Template infrastructure: partials, assets, and per-request context

App.Partials, HeadAssets, and ContextFunc are the three mechanisms that make it possible to deliver consistent output to all four audiences from a single content definition.

When you register a module in Forge, your templates need access to shared components -- navbar, footer, head -- and per-request data that is not part of the content type itself. Three mechanisms handle this: App.Partials, forge:head, and ContextFunc.

App.Partials and MustParseTemplate

Shared template components are registered on the app and available to all module templates.

app.Partials("templates/partials")

When a module template is parsed with app.MustParseTemplate, it automatically inherits the partials:

tmpl := app.MustParseTemplate("templates/post/detail.html")

The navbar, footer, and any other shared components are defined once and rendered consistently across all content types. A change to the navbar partial propagates everywhere without touching individual module templates.

forge:head

Assets -- favicons, stylesheets, scripts -- are registered in code rather than hardcoded in template files.

app.SEO(&forge.HeadAssets{
    Stylesheets: []string{"/static/app.css"},
    Links: []forge.HeadLink{
        {Rel: "icon", Type: "image/png", Sizes: "32x32", Href: "/favicon-32.png"},
    },
})

The forge:head template tag renders these in the <head> of every page. The result: asset paths are defined in one place, versioned with the binary, not scattered across template files where they can drift out of sync.

ContextFunc

Some data is needed by templates but is not part of the content type being rendered. A sidebar listing all published doc pages. A strip of recent devlog posts. Navigation state that depends on the current URL.

ContextFunc injects this data per request:

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 all templates for that module -- fetched once per request, not per template, not per endpoint.

The practical consequence: the docs section needs a sidebar listing all published DocPages and a "Latest Updates" strip with recent devlog posts. Without ContextFunc, each docs endpoint fetches and passes this data individually. With ContextFunc, it is handled once in main.go and available everywhere.

Why this matters across four audiences

These mechanisms ensure that the same content structure delivers consistent output regardless of which delivery channel is handling the request. Browser templates, API responses, and the content pipeline feeding .aidoc and /llms-full.txt all draw from the same registered partials and the same ContextFunc data.

Consistency at the infrastructure level means you do not maintain separate data-fetching logic for different audiences.