Petra plugins are small by design. A plugin can add template functions with
Funcs() and can parse helper templates with Apply().
That gives a plugin enough room to be useful, but it also means plugin helpers
can bypass html/template escaping when they return trusted template types.
This page describes the contract Petra currently follows.
For each new template set, Petra does this:
- Collect
Components(namespace, set)mounts and the render plugins required by those sets. - Copy
Template.FuncMapinto a new function map. - Add functions from required component-set plugins.
- Add functions from non-component plugins in the order listed in
Template.Plugins. - Attach the combined function map to the Go template.
- Apply required component-set plugins.
- Apply non-component plugins in
Template.Pluginsorder. - Compile mounted component sets and their private imports.
- Parse the page, layout, and app component templates.
ParseDir, ParseFS, and successful reloads all build a new template set.
They do not mutate the active parsed templates in place.
Component-set requirements override same-named functions from
Template.FuncMap.
Explicit plugin functions override required component-set functions. If two
explicit plugins return the same function name, the later plugin in
Template.Plugins wins.
Keep plugin order explicit when names overlap.
The built-in plugins use these names:
HTML()addsattrs,html, andjs.Markdown()addsMarkdownToHTMLand the callable{{Markdown}}helper.SVG()adds the callable{{SVG}}helper.Components(namespace, set)mounts exported templates from aComponentSet.
The _loadMarkdown and _loadSVG functions are implementation details used by
the helper templates. Do not call them from application templates.
NewComponentSet(id, files, root, opts...) describes a reusable component
library. Components(namespace, set) mounts that library into an app.
Definitions inside a component set are namespace-free:
{{define "TextField name label id attrs error?"}}
<label for="{{.id}}">{{.label}}</label>
<input id="{{.id}}" name="{{.name}}"{{range $k, $v := .attrs}} {{attrs $k $v}}{{end}}>
{{if .error}}<span class="error">{{.error}}</span>{{end}}
{{end}}
The app chooses the public namespace:
ui := petra.NewComponentSet(
"github.com/acme/petra-ui",
uiFS,
"components",
petra.Requires(petra.HTML()),
)
tmpl := petra.NewWithOptions(petra.Options{
Plugins: petra.Plugins{
petra.Components("UI", ui),
},
})Application templates call exported definitions through the mounted namespace:
{{UI.TextField "email" "Email" "email" (dict "type" "email") .Errors.Email}}
Export uses Go's casing rule. TextField is public. fieldLabel and
_fieldLabel are private.
A set can privately import another set:
base := petra.NewComponentSet("github.com/acme/petra-base", baseFS, "components")
kit := petra.NewComponentSet(
"github.com/acme/petra-kit",
kitFS,
"components",
petra.Import("Base", base),
)Templates in kit can call {{Base.Button "Save"}} if Button is exported by
base. The app does not get a Base namespace unless it mounts base too.
Use a live filesystem such as os.DirFS while developing a library. Add both
the application template folder and the library folder to HotReloadOptions.
Changes outside the application template root trigger a full template reparse,
so external component-set edits update cleanly even though Petra's selective
dependency graph remains app-template-root based.
Requires is for server-side render plugins only. Component sets do not manage
client assets, JavaScript imports, CSS files, or model types in v1.
See component set architecture for the parse pipeline, privacy rules, and package shape.
The built-in Markdown and SVG plugins cache rendered output inside closures
created by Funcs().
That means the cache belongs to one parsed template set. When Petra reparses
templates, it calls Funcs() again and the new template set gets a fresh cache.
Requests that are already using the old template set can keep using the old
cache until those requests finish.
Missing Markdown and SVG files fail closed. Petra returns an execution error and does not emit partial trusted HTML for that helper call.
MarkdownToHTML renders the string it receives each time. It does not use the
file cache.
These helpers are for repository-controlled content or HTML generated by your application:
htmlreturnstemplate.HTML.jsreturnstemplate.JS.MarkdownandMarkdownToHTMLreturntemplate.HTML.SVGreturnstemplate.HTML.
Do not pass user-authored content to those helpers as a sanitizer. They mark the
result as trusted for html/template.
The SVG class argument is also trusted input. Petra replaces an existing
class attribute in the raw SVG text; it is meant for app-owned class strings
such as Tailwind classes.
attrs is different. It validates the attribute name, escapes the value,
blocks on* and style attributes, and rejects unsafe URL schemes for URL
attributes. It is still best used with app-chosen attribute names.
A custom plugin should keep its trust boundary obvious:
- Return plain strings when
html/templateshould escape the result. - Return
template.HTML,template.JS, ortemplate.HTMLAttronly after the plugin has produced safe output itself. - Keep caches inside
Funcs()when they should reset on template reparse. - Avoid helper names used by Petra's built-ins unless overriding them is intentional.
The package examples include a small custom plugin.