1---2name: zotonic-template-creation3description: Use when creating, refactoring, or reviewing Zotonic template_compiler templates in priv/templates, including page/base templates, partials, category-specific templates, catinclude/catcompose usage, translations, template inheritance, template filters, scomps, and standard html/body/head setup for Zotonic sites and modules.4---56# Zotonic Template Creation78## First Pass910- Inspect nearby templates before editing. Preserve local structure, blocks, naming, and CSS class conventions.11- Look across related pages and categories before adding a one-off template or conditional. Express shared behavior with inheritance, reusable partials, category templates, `catinclude`/`catcompose`, or resource data; use a named-resource template only when that resource is genuinely exceptional.12- Put templates under the app or site `priv/templates` directory.13- Use Zotonic `template_compiler` syntax, which is Django-like but has Zotonic-specific tags, models, scomps, category-aware includes, and runtime compilation.14- Use semantic HTML and accessible attributes; avoid inline styles unless the value is truly dynamic.151617## Security1819- Treat template output as HTML unless proven otherwise. Escape data at trust boundaries.20- Properties from the `m.rsc` model are generally sanitized/pre-escaped and safe to render directly, for example `{{ id.title }}` or `{{ m.rsc[id].summary }}`.21- Do not assume all `m.rsc` values are safe: properties explicitly marked or documented as unsafe, such as names ending in `_unsafe`, must be escaped or sanitized before output.22- Output from other models must be escaped when rendered into templates unless that model explicitly documents the returned value as safe HTML. This includes request, identity, EXIF, medium metadata, and custom model output.23- Access query and request arguments with `q`, for example `{{ q.qs|escape }}`. Values from `q` always come from outside and must be considered unsafe.24- Values from `q` can be anything accepted by request/API handling, including strings, booleans, structured maps/lists, or upload records such as `#upload{}`; never bind an unsafe `q` value to a trusted variable name such as `id`.25- When a query id is needed as a resource, sanitize through the resource model: prefer `{% with m.rsc[q.id].id as id %}` over `{% with q.id as id %}`.26- Follow the Zotonic XSS guidance in `https://zotonic.com/cookbook/2325/security-templates-and-xss-prevention`: escape `q.*`, use sanitized controller/resource ids directly, and keep untrusted variables visibly named as untrusted, for example `qid`.2728## Template Layout2930- Make site page templates extend a common base, usually `{% extends "base.tpl" %}` or `{% extends "page.tpl" %}`.31- If a template uses `{% extends %}` or `{% overrules %}`, make that the first template tag. The template should then contain block overrides, not arbitrary top-level page markup.32- `extends`, `overrules`, `inherit`, `compose`, `catcompose`, `use`, and `useblock` accept extra arguments; the optional `with` keyword is accepted for compatibility.33- Use `{% inherit %}` inside an overridden block to render the inherited block body, optionally with extra variables.34- Use `{% compose %}`/`{% catcompose %}` when the caller needs to include a template and override named blocks inside it.35- Use fragments with `{% fragment %}`, `{% use %}`, and `{% useblock %}` for reusable pieces inside an inheritance tree.3637## Base Templates3839- A normal site base should include language, environment, Cotonic, module head/body hooks, JS collection, and script flush support.40- Put language on `<html>` using `z_language`, for example `lang="{{ z_language|default:"en"|escape }}"`; include `xml:lang` when the local site pattern does.41- Add an environment class on `<html>`: `class="environment-{{ m.site.environment }}"` or include it alongside other root classes.42- Include `{% all include "_html_head.tpl" %}` in `<head>` so modules can add Cotonic setup, SEO, theme initialization, feeds, and other head fragments.43- Include `{% all include "_html_body.tpl" %}` near the end of `<body>` so modules can add required body fragments.44- Include `{% include "_js_include.tpl" %}` when the site has a JS include partial.45- End the body with `{% script %}` so collected `{% javascript %}`, `{% wire %}`, and related scomp output is emitted.46- Add `data-cotonic-pathname-search="{% cotonic_pathname_search %}"` to `<body>` for Cotonic UI components.47- Use body classes like `page-{{ id.name }}` and `cat-{{ cat }}` for the current resource and its categories.48- Add `{% block html_attr %}`, `{% block body_class %}`, and `{% block body_attrs %}` or the local equivalents so pages can add attributes.49- For admin-like bases, follow `admin_base.tpl`: use `_language_attrs.tpl`, `zotonic-admin`, `_html_head_admin.tpl`, `_html_body_admin.tpl`, `data-cotonic-pathname-search`, admin JS includes, and `{% script %}`.5051## Includes And Subfolders5253- Prefix partial template names with `_` when they are not directly routed pages.54- Use subfolders to group related partials in larger sites, following patterns such as `cards/`, `header/`, `nav/`, `footer/`, `search/`, `page/`, `page-actions/`, `edit/`, `icons/`, `email/`, `pivot/`, or feature-specific folders.55- Keep path and category naming together for category-aware partials, for example `cards/card.tpl`, `cards/card.event.tpl`, `header/header.tpl`, `header/header.name.page_art_agenda.tpl`, `page/content-left.tpl`, `page/content-left.person.tpl`.56- Use `{% include "partial.tpl" arg=value %}` for a single known partial.57- For boolean include arguments, use the bare flag form, such as `{% include "partial.tpl" is_resultless %}`. The template compiler adds the `true` value, analogous to an Erlang proplist flag.58- Use `{% optional include "partial.tpl" %}` when the partial can be absent.59- Use `{% all include "_html_head.tpl" %}` or `{% all include "_admin_overview_filter_panel.tpl" %}` when all modules should contribute same-named templates in module priority order.60- Use include cache arguments (`max_age`, `vary`, `sudo`, `anondo`, `runtime`) only when needed; be explicit because they affect ACL/runtime behavior.6162## Category Templates6364- Use category-specific template filenames for resources and partials: `page.event.tpl`, `page.name.page_home.tpl`, `_admin_edit_content.person.tpl`, `cards/card.artwork.tpl`.65- Use `{% catinclude "base.tpl" id %}` when a resource category or unique resource name should choose the template.66- For a resource named `my_page_name` in category `news` under `article` under `text`, `{% catinclude "hello.tpl" id %}` searches `hello.name.my_page_name.tpl`, `hello.news.tpl`, `hello.article.tpl`, `hello.text.tpl`, then `hello.tpl`.67- When passing category names instead of a resource, put the most specific category last: ``{% catinclude "hello.tpl" [`text`, `article`] %}`` searches `hello.article.tpl`, then `hello.text.tpl`, then `hello.tpl`.68- Category names are mixed into the full basename before `.tpl`, preserving subfolders and prefixes. `catinclude "cards/card.tpl" event_id` looks for `cards/card.name.NAME.tpl`, `cards/card.event.tpl`, and `cards/card.tpl`.69- Use `{% all catinclude "partial.tpl" id %}` when all matching category templates from all modules must render, as in admin edit panels.70- Prefer `catinclude`/`catcompose` over large `if id.is_a...` switches when category-specific rendering is intended.7172## Translations7374- Keep template source strings in English.75- Wrap user-facing static text in `{_ Text _}`.76- Use `{% trans "Hello {name}" name=id.title %}` for translated strings with variable substitution.77- For translated tag/scomp/include arguments, use compiler-native translated literals: `text=_"Save"`, `title=_"Save this page."`, or `headline=_"Latest modified opportunities"`.78- In HTML attributes, embed normal translation tags, for example `placeholder="{_ Search _}"`.79- Use `z_language` for the current language. For language-specific resource URLs, use `{{ id.page_url with z_language = code }}`.80- Translation strings are extracted to POT files under `priv/translations/template/`, for example `priv/translations/template/sitename.pot`.81- Do not regenerate or commit POT files during normal feature work. POT files are generated on the `master` branch with `bin/zotonic pot zotonic`; if a feature/test command creates POT diffs, restore or leave them out unless the user explicitly asks to update POT files.82- Never update Zotonic core `.po` files directly; they are managed through Crowdin at `crowdin.com`.83- Merge site/user-module PO files with `msgmerge --backup=none --update priv/translations/nl.po priv/translations/template/sitename.pot` and validate with `msgfmt --check --output-file=/dev/null priv/translations/nl.po`.84- Use `_translations.tpl` for strings that must be extracted but are not otherwise visible in templates.8586## Tags And Filters8788- Read tag documentation in `doc/template-tags/` when using unfamiliar tags. Important files include `extends.md`, `overrules.md`, `include.md`, `all_include.md`, `catinclude.md`, `all_catinclude.md`, `compose.md`, `catcompose.md`, `fragment.md`, `use.md`, `useblock.md`, `inherit.md`, `trans.md`, `trans_ext.md`, `url.md`, `lib.md`, `image.md`, and `media.md`.89- Built-in tags handled by Zotonic/template_compiler include control and inheritance tags such as `for`, `if`, `block`, `extends`, `overrules`, `include`, `catinclude`, `all include`, `comment`, `with`, `cache`, `filter`, `spaceless`, `javascript`, and `trans`.90- Zotonic runtime built-in tags include `url`, `lib`, `lib_url`, `image`, `image_url`, `image_data_url`, and `media`.91- Unknown non-built-in tags are custom Zotonic scomps. Find them under `src/scomps/` as `scomp_<module>_<tag>.erl`; examples include `cotonic_pathname_search`, `wire`, `button`, `pager`, `menu`, and `live` depending on enabled modules.92- Template filters are Erlang modules under `src/filters/` named `filter_name.erl`. The template filter `{{ value|summary:120 }}` maps to `filter_summary:summary(Value, 120, Context)`.93- Check `apps/zotonic_mod_base/src/filters/` first for core filters, then app/module `src/filters/` directories for module-specific filters.94- Filters should have a `-moduledoc` in their `src/filters/filter_*.erl` source file; use that source documentation as the reference.95- `default`, `default_if_none`, and `default_if_undefined` are inlined by `template_compiler`; many other filters call `filter_<name>` modules.96- Use escaping filters such as `escape`, `escapejs`, `escapejson`, `sanitize_html`, `sanitize_url`, and `urlencode` at trust boundaries.979899## Scomps100101- Scomps are screen components: server-side template tags that render HTML, JavaScript, or wire actions from Erlang.102- Use scomps as normal template tags, for example:103 ```django104 {% button text=_"Save" action={submit} %}105 {% wire id="form" type="submit" postback={save id=id} %}106 {% pager result=result dispatch="page" %}107 {% menu id=`main_menu` %}108 ```109- Unknown non-built-in tags are usually scomps. Find their implementation under `src/scomps/` as `scomp_<module>_<tag>.erl`.110- Scomps should have a `-moduledoc` that documents accepted arguments, generated markup, emitted JavaScript, postbacks, and security assumptions.111- Prefer existing scomps for common Zotonic behavior such as buttons, wires, validation, live updates, sortable lists, menus, tabs, pagers, and Cotonic integration.112- Keep scomp arguments explicit and translated where user-facing, for example `text=_"Delete"` and `title=_"Delete this page."`.113- If a scomp collects JavaScript or wires, ensure the base template ends with `{% script %}`.114115## Forms116117- Use normal HTML forms and wire them with Zotonic when the result should be handled by Erlang: ``{% wire id="contact-form" type="submit" postback={contact []} delegate=`mod_contact` %}``.118- Forms that submit by postback normally use `method="post" action="postback"` and have a stable `id`.119- Use `{% button %}` or action `{submit}` when the local template pattern uses Zotonic buttons instead of raw submit buttons.120- In the receiving `event(#submit{}, Context)`, fetch fields with `z_context:get_q/2`, `z_context:get_q_all/1`, or `z_context:get_q_validated/2`; never trust client-side validation alone.121- Use `z_context:get_q_all_noz/1` or `z_context:get_q_map_noz/1` when processing ordinary form fields and excluding Zotonic internal parameters.122- Add validation with `{% validate %}` next to the field being validated. The validation tag emits LiveValidation JavaScript and also enables server-side validation for submit/postback handling.123124```django125{% wire id="contact-form" type="submit" postback={contact []} delegate=`mod_contact` %}126<form id="contact-form" method="post" action="postback">127 <input id="mail" name="mail" type="email">128 {% validate id="mail" type={presence} type={email} %}129130 <textarea id="message" name="message"></textarea>131 {% validate id="message" type={presence failure_message=_"Please enter a message."} %}132133 <button type="submit">{_ Send _}</button>134</form>135```136137- The `id` argument of `{% validate %}` is the input element id. Use `name="field_name"` when the submitted field name differs from the element id.138- Multiple `type={...}` arguments can be used on the same field, for example `type={presence} type={email}`.139- Common validation options include `failure_message`, `valid_message`, `message_after`, `only_on_blur`, `only_on_submit`, `wait`, `trigger`, and `target`.140- Use generated ids such as `id=#email name="email"` when a partial can be rendered multiple times on one page.141- File inputs can use the `presence` validator; submitted uploads arrive as `#upload{}` values.142- Include `_js_include.tpl`/admin JS includes and end the page with `{% script %}` so LiveValidation, validators, wires, and form postbacks are initialized.143144## Validators145146- Template validators are implemented by Erlang modules in `src/validators/` named `validator_<module>_<name>.erl`.147- Validator type names in templates map to those modules. `{% validate id="email" type={email} %}` resolves to `validator_base_email`; module-specific validators include examples such as `validator_admin_identity_username_unique`.148- Core validators are in `apps/zotonic_mod_base/src/validators/`; inspect their `-moduledoc` before using unfamiliar arguments.149- Common core validators include `presence`, `email`, `length`, `numericality`, `format`, `date`, `json`, `acceptance`, `confirmation`, `postback`, `name_unique`, and `page_path_unique`.150- Validators generally implement `render_validator/5` for client-side LiveValidation setup and `validate/5` for server-side checks.151- A validator can return extra validation args from `render_validator/5`; those args are passed to `validate/5` during server validation.152- The `postback` validator performs custom server-side validation through a notification or delegate; use it for checks that need database lookups, ACL checks, or external state.153154```django155<input id="username" name="username" type="text">156{% validate id="username" wait=400157 type={presence}158 type={postback event="validate_username"} %}159```160161- For a custom validator, add `src/validators/validator_mymodule_slug.erl`, export `render_validator/5` and `validate/5`, include `zotonic.hrl`, and document all template arguments in `-moduledoc`.162- Client-side LiveValidation is for responsiveness; all security and data integrity checks must also happen server-side in the validator, `event/2`, model, or controller.163164## Source Documentation165166- Filters, scomps, validators, models, and modules should always include a `-moduledoc` in their Erlang source file.167- Use those `-moduledoc` entries as the authoritative local documentation for template-facing behavior.168- Do not rely on `doc/_build`; it was generated by older documentation systems and is not expected to exist.169170## Models171172- Access template models through `m`, using the model name without the `m_` prefix: `m.rsc`, `m.search`, `m.req`, `m.acl`, `m.config`, `m.category`, `m.media`, `m.edge`, and module-specific models.173- Model paths use dot/index notation. Examples: `{{ m.rsc[id].title }}`, `{{ m.acl.user }}`, `{{ m.req.host|escape }}`, `{{ m.config.site.title.value }}`.174- Use the shorthand `id.title`, `id.summary`, `id.o.haspart`, and `id.s.author` only when `id` is the sanitized page resource or came from `m.rsc`.175- Use search models for resource lists and prefer the `::` payload operator for new search calls: `{% for id in m.search.query::%{ cat: ["news"], pagelen: 10 } %}`.176- Use `{% with %}` to avoid repeating expensive model lookups, especially searches: `{% with m.search.query::%{ cat: ["news"], pagelen: 10 } as latest_news %}`.177- Models are implemented in `src/models/` as `m_name.erl` modules with the `zotonic_model` behaviour. Core models live in `apps/zotonic_core/src/models/`; module models live in each module's `src/models/`.178- Models should have a `-moduledoc` in their `src/models/m_*.erl` source file; use that source documentation as the reference. The old `doc/_build` generated docs are an obsolete artifact and should not be relied on.179- Before using output from a less familiar model, read its source documentation and check whether it performs ACL checks and whether returned values are safe for direct HTML output.180181## Model Payloads182183- Use the `::` operator to pass a structured payload to a model call. The expression after `::` is passed as the model payload instead of being part of the path.184- Prefer maps for payloads: `m.search.query::%{ cat: ["article"], is_published: true, sort: ["-created"], pagelen: 20, page: q.page }`.185- The path before `::` still selects the model and operation. In `m.search.paged.query::%{ text: q.qs }`, the path is `[paged, query]` and the payload is the map.186- For the search model, prefer `::` over the older tuple/list query syntax. It is clearer, easier to extend, and matches model API payload handling.187- Older tuple-style search syntax, such as `m.search[{latest cat="text" pagelen=10}]`, is still encountered in existing templates and docs but should not be copied for new code.188- Use tuple-style only when maintaining existing code that depends on the deprecated search representation or when the target model explicitly documents tuple input.189- Treat payload values from `q` as unsafe. A structured payload can safely pass `q.page` to the model for validation, but never render `q.*` directly without escaping.190191## Dynamic Template Rendering192193- The `m_template` model renders a template dynamically through the model path `/render/...`, exposed to browser code as `bridge/origin/model/template/get/render/<template-path>`.194- The request payload is added to the render context as query arguments. It does not become ordinary top-level template variables: a payload `{ text: "alice" }` is available as `q.text`, not `text`.195- When a partial must work both as a normal include and as a dynamic template-model response, explicitly fall back to the query argument: `{% with text|default:q.text as search_text %}`.196- Dynamic rendering keeps the caller's context, so model calls in the rendered template retain their normal ACL behavior. It does not add an authorization boundary of its own; use a server-side delegate when the operation needs an explicit permission check beyond the called models' ACL checks.197- Treat every `q.*` value supplied to a dynamically rendered template as untrusted. Pass it to models that validate their input, and escape it whenever it is rendered into HTML, attributes, URLs, or JavaScript.198199## Resource And URL Patterns200201- Prefer `{{ id.title }}`, `{{ id.summary }}`, `{{ id.body }}`, and `{{ id.depiction }}` when `id` is the sanitized page resource.202- Use `m.rsc[unsafe_id]` when the resource id comes from an untrusted source; Zotonic will sanitize through the model.203- Use `{{ m.rsc.unique_name.page_url }}` for named page resources.204- Use `{% url dispatch_name id=id %}` for dispatch/controller URLs.205- Do not hard-code internal URLs when a dispatch rule or resource `page_url` exists.206- Use `{% image id mediaclass="..." alt=id.title %}` or `{% media id %}` for media; define image sizes in `priv/templates/mediaclass.config`.207208209## CSS And Bootstrap210211- Put source styles in `priv/lib-src/` and generated browser assets in `priv/lib/`; templates should include generated CSS with `{% lib "css/file.css" %}` or module/site include templates.212- Do not edit generated CSS in `priv/lib/` when a matching SCSS/LESS source exists. Edit the source file and rerun the relevant Makefile.213- Use the local build pattern. Most style builds are run from the source directory, for example `make -C apps/zotonic_mod_bootstrap/priv/lib-src`, `make -C apps/zotonic_mod_bootstrap/priv/lib-src/bootstrap`, `make -C apps/zotonic_mod_admin/priv/lib-src/admin-bootstrap5`, or `make -C apps_user/zotonicwww2/priv/lib-src`.214- SCSS builds use Dart Sass via the `sass` command; older icon/base styles may still use `lessc`. If a Makefile defines `SASS ?= sass`, override it with `make SASS=/path/to/sass` only when needed.215- `zotonic_mod_bootstrap` keeps Bootstrap sources under `priv/lib-src/bootstrap/scss` and Bootstrap 3 compatibility shims under `priv/lib-src/bootstrap/compat`. Its build emits `priv/lib/bootstrap/css/bootstrap.css` (Bootstrap 5 plus Bootstrap 3 compat), `bootstrap5.css` (Bootstrap 5 without compat), `bootstrap3-compat.css` (compat only), theme CSS, and minified variants.216- Use `bootstrap.css` when existing templates still use Bootstrap 3 class names and need the compatibility layer. Use `bootstrap5.css` only when templates are written for Bootstrap 5 classes or when a site includes compatibility separately.217- The admin has separate Bootstrap builds: `priv/lib-src/admin-bootstrap3` is kept for old Bootstrap 3 output, and `priv/lib-src/admin-bootstrap5` builds `priv/lib/css/admin-bootstrap5.css` and `.min.css` with Bootstrap 5, admin overrides, and the admin Bootstrap 3 compatibility file.218- Admin theme and component styles live in `apps/zotonic_mod_admin/priv/lib-src/zotonic-admin/scss`; prefer CSS custom properties from `_theme.scss`/`_variables.scss` for colors so light/dark themes keep working.219- Bootstrap 3 compatibility files should stay in separate `compat/` directories. Keep them SCSS-friendly with variables and nested selectors where that improves maintainability.220- When adding or changing template classes, first check whether the project is relying on Bootstrap 3 compatibility. Avoid changing large template class sets just to satisfy Bootstrap 5 if compat CSS is intended to preserve them.221- For dark/light theme support, prefer existing CSS variables and add new variables near the theme files rather than hard-coded colors in component partials.222- After SCSS/LESS edits, rerun the relevant Makefile and verify both normal and `.min.css` outputs if the build produces minified files.223224## Cotonic225226- Cotonic is Zotonic's browser-side JavaScript runtime for isolated components, workers, and MQTT-style publish/subscribe messaging.227- Cotonic exposes `cotonic` on the page, including `cotonic.ready`, `cotonic.broker.publish`, `cotonic.broker.subscribe`, `cotonic.broker.call`, `cotonic.spawn`, `cotonic.spawn_named`, and MQTT helpers such as `cotonic.mqtt.matches`, `fill`, and `extract`.228- Cotonic topics are slash-separated MQTT-style topics. Use `+` and `#` wildcards in subscriptions; named wildcards such as `+id` can be extracted by Cotonic helpers.229- Zotonic bridges the browser broker to the server over the origin MQTT bridge. Server topics are commonly addressed with `bridge/origin/...`, for example `bridge/origin/model/rsc/event/{{ id }}/delete`.230- Include Cotonic through Zotonic's standard head/JS includes. Do not hand-roll the low-level bridge setup unless working on `mod_base` itself.231- Ensure the base template includes `{% all include "_html_head.tpl" %}` or `_html_head_admin.tpl`; these include Cotonic head setup such as `_html_head_cotonic.tpl`.232- Ensure the JS includes load `cotonic/cotonic.js` and the Zotonic wired app when the site uses declarative data attributes or wires.233- Add `data-cotonic-pathname-search="{% cotonic_pathname_search %}"` to `<body>`; Zotonic's wired JS warns when it is missing and Cotonic UI/location handling depends on it.234- Use `cotonic.ready.then(function() { ... })` before calling Cotonic APIs from inline page JavaScript that must run after initialization.235236## Cotonic Data Attributes237238- Prefer Cotonic data attributes for simple declarative behavior instead of ad-hoc JavaScript event handlers.239- Use `data-onclick-topic="topic"`, `data-onsubmit-topic="topic"`, and `data-oninput-topic="topic"` to publish DOM events to Cotonic topics. Example: `<a href="#back" data-onclick-topic="model/location/post/redirect/back">`.240- For live search/filter forms, use `data-onsubmit-topic="model/location/post/qlist/submit"` and `data-oninput-topic="model/location/post/qlist/submit"` on a `GET` form.241- Event data attributes normally prevent default behavior and stop propagation. Use `data-onclick-cancel`, `data-onsubmit-cancel`, or `data-oninput-cancel` when local behavior needs different cancellation semantics.242- Use Cotonic location model topics for URL behavior: `model/location/post/redirect/back`, `model/location/post/qlist/submit`, `model/location/post/push`, `model/location/post/replace`, and their silent variants when appropriate.243- Use storage topics instead of direct storage access when cooperating with Cotonic components: `model/localStorage/get/+key`, `model/localStorage/post/+key`, `model/sessionStorage/get/+key`, and matching `event/+key` topics.244- Use UI topics such as `model/ui/insert/+key`, `model/ui/update/+key`, `model/ui/delete/+key`, and `model/ui/event/dom-updated/+key` for DOM updates managed by Cotonic UI code.245- Escape any template values placed inside data attributes, especially topic fragments or payload-like values that include `q.*` or non-resource model output.246- Keep data attributes readable and topic-oriented; if a behavior needs complex state, use a scomp, a Cotonic model, or a dedicated JavaScript module.247248## JavaScript And Interactivity249250- Put inline template-collected JavaScript inside `{% javascript %}...{% endjavascript %}` and ensure the base has `{% script %}`.251- Use Zotonic declarative behavior (`{% wire %}`, scomps, Cotonic widgets, `do_*` classes) when nearby templates use it.252- Include Cotonic assets through the site `_js_include.tpl` and module head/body hooks, not by duplicating low-level bridge snippets.