- docs/backend: admin controllers, forms, lists and filters, relation manager, users and permissions, settings, partials and widgets, admin SPA - docs/services: storage, outbound HTTP, realtime, Web Push, search, parity testing and the Frontend and AJAX (not provided) page - Examples for cabana (with testdata/docs YAML), fetchguard, lighthouse and its centrifugo driver, flare, beachcomber and typesense, tide; lighthouse and beachcomber TestDocs* regions run on their Postgres harnesses - concept map rows link their guide pages and the not-provided rows the Frontend and AJAX page; index lists Backend, Database and Services - TestDocsRequiredPages asserts the D-08 section order
4.6 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Partials and widgets | Extend admin screens with server-rendered partials, plugin JavaScript and CSS, form widgets backed by server actions, and custom toolbar buttons. | backend | 70 |
Partials and widgets
WinterCMS controllers extend their screens with partials, addJs and addCss, custom form widgets and toolbar buttons that call AJAX handlers. The SummerCMS admin is a single-page app, so cabana keeps these extension points in a form the SPA can render safely: partials arrive as an allowlisted node tree, plugin scripts are declared files, and every button or widget runs a server action through a cabana-owned route.
Server-rendered partials
Two places accept a partial:
headerPartial: <name>inconfig_list.yaml, a strip above the list;- a
type: partialfield withpath: <name>infields.yaml.
Both render {ConfigDir}/_<name>.htm with Go's html/template. WinterCMS $/ and ~/ partial paths are not supported. The template's data is .Data, the value the controller's pact.AdminPartialData returns for that partial name; for a form partial on an existing record, cabana passes the record it loaded through the controller's pact.FormExtendQuery scope. trans "<key>" translates a phrase key in the request locale.
A statistics strip above a list, using the SPA's partial style classes:
<dl class="summer-stats">
{{- range .Data.Items -}}
<div class="summer-stat"><dt class="summer-stat__label">{{ trans .Label }}</dt><dd class="summer-stat__value">{{ .Count }}</dd></div>
{{- end -}}
</dl>
The view model must be a struct built for the template. cabana refuses a view model that holds the controller's model or any other GORM model, anywhere inside it, and refuses pre-escaped html/template content types, so every record value stays escaped.
The rendered HTML is parsed and walked through an allowlist before it reaches the SPA: script, style, iframe, form and similar elements are removed with their content, unknown elements are unwrapped, id, style and event handler attributes are dropped, and links and images must be same-origin paths. Output is capped at 64 KiB, 2000 nodes and a depth of 32. The cabana README lists the allowed elements, attributes and style classes.
Plugin JavaScript and CSS
A controller that implements pact.AdminClientAssets names .js, .mjs and .css files under its plugin's assets/ directory, the Go form of addJs and addCss. They are read from the embedded tree at start-up (a missing file stops it) and served from <prefix>/assets/{vendor}/{plugin}/... with a content hash in the URL, the admin Content-Security-Policy (script-src 'self') and nosniff. Only declared files are reachable; the YAML and templates never are.
Plugin CSS may use only the SPA's public CSS variables (--c-bg, --c-surface, --c-text, --c-primary and the others the cabana README lists), which switch with dark mode. Do not hardcode colours and do not rely on the SPA's utility classes.
Form widgets
A type: widget field puts a plugin custom element in the form and connects it to a server action:
lookup:
label: acme.blog::lang.posts.lookup
type: widget
widget: acme-blog-lookup
action: lookup
fill: [title, slug]
widgetis the custom element's tag, which must start with the plugin's{vendor}-{plugin}-prefix. A plugin script declared throughpact.AdminClientAssetsdefines the element.actionnames an action the controller registers throughpact.HasAdminActions.filllists the writable scalar fields of the same form the action may write back.
The SPA posts the widget's values to .../widgets/{field}. cabana checks the CSRF header, the controller's and the action's permissions and the record scope, then calls the action's Run with a pact.AdminActionInput. The answer's pact.AdminActionResult carries a message and the fill values; keys outside fill and values that are not scalars are dropped before the response is written. An action may return a cabana.ValidationError to answer 422 on a field.
Toolbar actions
Names in toolbar.buttons of config_list.yaml, other than the built-in create and delete, are actions the controller registers through pact.HasAdminActions. Each needs a label. A toolbar action runs with an empty body and no record IDs, so it can never become an unscoped lookup of IDs the client chose. The list schema lists only the actions the administrator may run.
An unknown action name, a widget tag outside the plugin's prefix or a fill key that is not a writable scalar field stops the start-up.