# Public shortcodes

Data, variables, flow, content, URLs and public utility shortcodes.

## Data

### `[#data resource#]`
Loads all records of a resource into `data.<resource>`.

```
[#data articles#]
[#data articles sort=date|desc#]
[#data articles filter=published:1#]
[#data articles search=nimbly#]
[#data articles var=featured filter=featured:yes#]
[#data users.abc123#]           → loads single record into data.users.abc123
[#data users uuid=abc123#]      → same
[#data articles filter=published:1 sort=date|text|desc limit=20 offset=40#]   → the third page of 20
```

Parameters:
- `sort` - `field|asc`, `field|desc`, multiple: `date|desc,title|asc`
- `filter` - `field:value`, multiple: `published:1,status:new`, negation: `status:!draft`, or: `status:new||todo`
- `search` - full-text search across all fields
- `var` - custom variable name instead of `data.<resource>`
- `op` - `read` (default) or `list` (UUIDs only)
- `limit`, `offset` - one page of the list: at most `limit` records, after skipping `offset`. The number of records before the cut is in `data.<resource>-total` (or `<var>-total`)

With `limit` or `offset` the list is answered without reading every record. For each field the `filter` and the `sort` name, the values of all records are kept together in a cache file (`ext/data/.tmp/cache/_data/list/<resource>/<field>`), built the first time the field is asked for; only the records of the page are read. A write adds a line to those files, a Git pull makes them build again, and `./nimbly data:cache:clear` removes them. Use it for every list of a large resource; a sort with a type (`date|text|desc`, `price|num|asc`) is the fast one. `search` still reads every record. From PHP: `data_query($resource, ['filter' => …, 'sort' => …, 'limit' => 20, 'offset' => 40])` in `core/lib/data-list.php` returns `['records' => …, 'total' => …]`.

### `[#repeat data.articles#]`
Iterates over a data variable, rendering the template with the same name for each item.

```
[#repeat data.articles#]
[#repeat data.articles tpl=article-card#]
[#repeat data.articles limit=3#]
[#repeat data.articles filter=published:1#]
[#repeat record.related_items csv tpl=related-item#]
[#repeat data.articles var=post#]   → item variable named "post" instead of "item"
```

Inside the template, each record's fields are available as `item.<field>`:

```html
<h2>[#get item.title#]</h2>
<p>[#get item.intro#]</p>
```

Also available per iteration:
- `item.ix` - numeric index (0-based)
- `item.x` - record key
- `item.key` - record key (string)

For comma-separated UUID reference fields, do not create a library just to load a subset. Load the target resource once with `[#data#]`, then repeat the UUID list with `csv` and read from the loaded resource variable:

```html
[#data participants#]
[#repeat stories.participants csv tpl=story-participant-item#]
```

```html
<!-- ext/tpl/story-participant-item/index.tpl -->
<a href="[#get data.participants.[#item.key#].link#]">
    [#get data.participants.[#item.key#].name#]
</a>
```

This uses the core data layer and its caching behavior, preserves the source record's UUID order, and avoids custom backend code for simple relationship rendering.

### `[#data-sort data.articles sort=title|string|asc#]`
Sorts an existing data variable in place.

```
[#data-sort data.articles sort=date|desc#]
[#data-sort data.articles sort=title|string|asc#]
[#data-sort data.articles sort=price|numeric|asc#]
```

### `[#data-join a b#]`
Merges `data.a` and `data.b` into `data.join`. Each record gets a `resource_type` field set to its source resource name.

### `[#data-count resource#]`
Outputs the number of records in a resource.

---

## Variables

### `[#set key=value#]`
Sets a template variable.

```
[#set page-title="About us"#]
[#set active-nav=about#]
[#set body-classes=editor-role append#]    → appends with a space separator
[#set tags=news append=,#]                 → appends with a custom separator
[#set key=value session#]                  → persists in session
[#set key=value overwrite#]                → replaces value even if already set
```

The `append` param concatenates rather than sets. Without a separator value it
inserts a space between the existing value and the new one; supply a character
to use a different separator. Useful for building CSS class lists or
comma-separated strings:

```
[#set body-classes=site-body#]
[#set body-classes=editor-role append#]    → "site-body editor-role"
[#set body-classes=dark-mode append#]      → "site-body editor-role dark-mode"
```

**`[#set#]` does not overwrite by default.** If the variable already has a
value, a plain `[#set#]` is a no-op. This makes it easy to define fallback
values in shared/core templates while letting route templates override them
first. Only add `overwrite` when you explicitly want to replace an existing
value (e.g. when passing data into a reusable template component).

### `[#get varname#]`
Outputs a value. `[#get#]` is the unified value accessor.

Resolution order:
1. Flat variable lookup: session variables, then system variables. The URL and cookies are read only when the template asks for them with `from` (see below)
2. Dot-path lookup into nested system variables, using the longest matching variable prefix
3. Data lookup fallback for `resource.uuid.field` paths
4. Automatic i18n resolution when the result is an object keyed by configured language codes

Request input is opt-in: `from=url` reads the query parameter with the same name, `from=cookie` a cookie, `from=url,cookie` (or `url|cookie`) both in the order given, and `from=(any)` the cookie and then the URL. A page variable with that name always wins. `[#if#]` accepts the same `from` for its conditions.

```
[#get page-title#]
[#get item.title default="Untitled"#]
[#get q from=url#]
[#get language#]
[#get data.settings.theme#]
[#get users.abc123.name empty="Unknown"#]
[#get item.title lang=nl#]
[#get item json#]
```

Legacy `[#get-key#]`, `[#jget#]`, `[#get-i18n#]`, and `[#lookup#]` patterns should be migrated to `[#get#]`.

### `[#fmt var=data.articles json#]`
Formats a variable for output.

```
[#fmt var=data.records json#]              → JSON encode
[#fmt var=myvar empty={} json#]            → JSON with fallback
[#fmt val=item.date type=date fmt="d-m-Y"#]
[#fmt val=item.body type=html#]            → strips all tags
[#fmt val=item.body type=plain#]           → strips tags and decodes entities
[#fmt val=item.size type=bytes#]           → human-readable bytes
[#fmt val=item.hours type=hours#]          → 3h 30m, 6m, 37h (whole minutes)
[#fmt val=item.created type=ago#]          → "3 days ago"
[#fmt val=[#data-count users#] type=number round=-2 round_mode=floor#]
[#fmt val=item.invoice type=file#]         → file download markup
[#fmt val=item.order type=pad length=4#]   → zero-padded number
```

Types: `text`, `plain`, `html`, `date`, `ago`, `json`, `bytes`, `hours`, `number`, `boolean`, `image`, `file`, `pad`

### `[#count varname#]`
Outputs the number of items in an array variable.

```
[#count data.articles#]
```

---

## Flow control

### `[#if condition action#]`

```
[#if user=(empty) redirect=login#]
[#if language=en tpl=intro-en#]
[#if status=published echo=yes#]
[#if user=(not-empty) tpl=dashboard tpl_else=login#]
[#if score=0 echo_else=[#get score#]#]
[#if not role=admin redirect=home#]
[#if lang=en or lang=nl tpl=western-layout#]
```

Conditions: `key=value`, `key=(empty)`, `key=(not-empty)`
Actions: `tpl=`, `tpl_else=`, `echo=`, `echo_else=`, `redirect=`
Modifiers: `not`, `or`, `and`

**`[#if#]` is always a single self-closing tag - there is no block form
(`[#if#]...[#/if#]`), and this is by design.** Templates contain no business
logic. Conditional content always lives in a separate template referenced by
`tpl=`. Never write block-style `[#if#]…[#/if#]` in a template.

### `[#redirect url#]`
Redirects immediately.

```
[#redirect login#]
[#redirect [#base-url#]/dashboard#]
```

---

## URL and path

### `[#base-url#]`
Returns the base URL path of the installation. Use this for all internal links.

```html
<a href="[#base-url#]/about">About</a>
<link href="[#base-url#]/ext/static/app.css">
```

**In JavaScript**, `[#base-url#]` is not available as a template tag. Use `nb.base_url` instead - it is set by core before any page scripts run. This is critical when building image URLs or API paths dynamically, especially when the site runs under an alias/subdirectory (e.g. staging). Never hardcode `/img/` or `/api/` as a bare path in JS.

```js
// wrong - breaks on staging aliases
return '/img/' + uuid + '/240w';

// correct
return nb.base_url + '/img/' + uuid + '/240w';
```

### `[#base-path#]`
Returns the filesystem path to the installation root. For server-side file includes.

### `[#url#]`
Returns the current full URL.

### `[#is-url segment#]`
Sets `is-url` to `true` if the current URL starts with the given segment. Use for active nav states.

```
[#is-url about#]
[#if is-url=true echo=active#]

[#is-url (home) =#]   → exact match for homepage
```

## Content

### `[#get-html field#]`
Outputs an editable HTML content field. Supports inline admin editing.

```
[#get-html main_text#]
[#get-html content.home.intro default="<p>Edit this text</p>"#]
```

Links in content are written from the site's root, `/contact` or `/en/article/deep-space`, without `[#base-url#]`. Where the site runs in a subfolder (staging under an alias), `get-html` puts the base path in front when the page is shown, and the editor takes it off again when it saves, so the stored link works on every environment. A link that already starts with the base path, and a link to another site, are left as they are.

I18n fields are resolved automatically by `[#get#]` and `[#fmt#]` when the value is an object keyed by configured language codes. Use `lang=` to force a language.

```
[#get item.title#]
[#get item.title lang=en#]
[#fmt var=item.title type=plain lang=nl#]
```

### `[#text Label_key#]`
Outputs a translated UI label from `.po` files. Use underscores for spaces in the key.

```
[#text Search#]
[#text Save_changes#]
[#text No_results_found#]
```

Translations live in `ext/data/.i18n/text.<lang>.po`.

### `[#cfield field#]`
Resolves the fully-qualified dot-path of a content field in the current template context. Used with `[#get-html#]` for static, inline-editable page content stored in the `.content` resource.

The `.content` resource is a key-value store for editable HTML blocks that are not tied to any data record. Each block is identified by `content.<page>.<field>`. When `[#get-html#]` targets a `.content` path that does not exist yet, it auto-creates the record and outputs the `default` value.

```html
<!-- In ext/tpl/about-intro/index.tpl -->
[#get-html content.about.intro default="<p>Edit this text</p>"#]
[#get-html content.about.body default="<p>Body text here</p>"#]
```

`[#cfield field#]` resolves the field path from the current template context, useful when the same template is used in multiple places:

```html
[#get-html [#cfield intro#] default="<p>Edit this</p>"#]
```

This pattern requires the user module to be active (loaded automatically).

### `[#get-img-html uuid sizes="..." mode=w#]`
**Preferred way to render images.** Generates a responsive `<img>` with `srcset`, `sizes`, lazy loading, and anti-upscale dimension guards. Always use this over a manual `<img>` tag.

```
[#get-img-html [#record.main_img#] sizes="sm-100,md-50,lg-33"#]
```

**`sizes` param** - comma-separated breakpoint-percentage pairs. Each token is `breakpoint-vw` where `vw` is the percentage of viewport width the image occupies at that breakpoint and above. Omitted breakpoints fall back to `100vw`.

| Example | Meaning |
|---|---|
| `sm-100,md-50` | Full width on mobile, half on md+ |
| `sm-100,md-33` | Full width on mobile, one-third on md+ |
| `sm-100,md-50,lg-25` | Full → half → quarter |

**`mode` param** - controls how the image is sized server-side:

| Mode | URL shape | Result |
|---|---|---|
| `w` (default) | `480w` | Resized to that width, height proportional |
| `c` | `480c` | Resized to that height, width proportional - good for consistent-height layouts |
| `w?ratio=1.0` | `480w?ratio=1.0` | Square crop (centre) - use for avatars, thumbnails |

Square crop example:
```
[#get-img-html [#record.avatar#] sizes="sm-50,md-25" mode=w?ratio=1.0#]
```

The inline `max-width`/`max-height` styles on the generated `<img>` are only anti-upscale guards - they do not interfere with CSS `object-fit: cover` or grid layouts when images are larger than their display size (which they always are for real content).

---

## HTML page

### `[#html#]`
Renders the full HTML page shell (doctype, head, body). Place at the end of route templates.

The following variables influence the HTML shell when set before `[#html#]`:

| Variable | Description |
|---|---|
| `page-title` | Sets the `<title>` tag and OG/Twitter title |
| `body-classes` | Adds CSS classes to the `<body>` tag |
| `page-settings-link` | Points the page actions pill's settings button at the admin edit page for the current record, instead of the page settings modal. Use this on detail pages to allow quick admin access from the frontend. Example: `[#base-url#]/nb-admin/articles/[#record.uuid#]`. Its label becomes "Edit <resource>" (e.g. "Edit article"). |
| `page-settings-label` | Overrides that label. |
| `page-actions` | Set to `off` to hide the page actions pill on a page without page context. It never shows in the admin. |
| `page-description` | Overrides the site-wide meta description for this page. Falls back to the site config description. |
| `og-image` | Image for social sharing. Accepts a **UUID** (expanded to an absolute `/img/UUID/1200w` URL), a **relative path** (`img/og-card.png`), or an **absolute URL**. Falls back to the project default set in `ext/tpl/meta/index.tpl`. |
| `og-type` | OG type for this page. Defaults to `website`. Use `article` for content detail pages. |
| `page-canonical-url` | Overrides the normalized canonical URL and matching `og:url`. Set it before `[#html#]`. |

```
[#set page-title="[#get record.title#]"#]
[#set og-type=article#]
[#set og-image=[#record.main_img#]#]
[#set page-description="[#fmt var=record.main_text type=html max_length=160#]"#]
[#set page-settings-link="[#base-url#]/nb-admin/event/[#record.uuid#]"#]
[#set body-classes=site-body#]
[#html#]
```

The project default OG image is set once in `ext/tpl/meta/index.tpl` without `overwrite`, so per-page values always take priority. Two image utilities are available as shortcodes:

- `[#img-url UUID-or-path-or-URL#]` - normalises any image reference to an absolute URL (useful outside meta, e.g. for JSON-LD)
- `[#first-img-uuid var=record.main_text#]` - extracts the UUID of the first embedded image from an HTML field; returns `(empty)` when none is found

## Importing resource records from DOCX

Resource add forms can expose the Core **Import document** action. It accepts a
`.docx`, extracts its paragraphs and hyperlinks, and sends the bounded document
text to the configured OpenAI import endpoint to propose values for the
resource's fields. Editors review the populated form before saving; importing
does not create a record by itself.

DOCX extraction requires PHP's ZIP extension because an Office Open XML
document is a ZIP archive. Every CLI and web/FPM runtime that can execute the
import or its tests must provide `ZipArchive`. Debian and Ubuntu installations
normally install it with the version-matched `php-zip` package; the Nimbly dev
and production Docker images include `php-zip` explicitly. Verify a manual
server with:

```bash
php -r 'echo class_exists("ZipArchive") ? "available\n" : "missing\n";'
php core/tests/docx-test.php
```

Do not infer web availability from CLI alone when CLI and PHP-FPM load separate
configuration trees. Confirm the extension in both runtimes after installation
and verify the document-import action through the web application.

## PDF documents

Install the core PDF module dependency with `php core/cli/nimbly.php module:install pdf`.
Application routes provide their project-specific HTML and use the core PDF
library for rendering, content-addressed caching, and safe browser downloads:

```php
load_library('pdf');
$pdf_bytes = pdf_render_cached_document([
    'paper_size' => 'a4',
    'orientation' => 'portrait',
    'margins' => ['top' => 20, 'right' => 18, 'bottom' => 20, 'left' => 18],
    'pages' => [$document_html],
]);
pdf_send_download($pdf_bytes, 'Document-123.pdf');
exit();
```

`pdf_render_document()` always renders fresh bytes. Prefer
`pdf_render_cached_document()` for downloads: its cache identity includes the
complete document configuration and core renderer files, so changed content or
renderer code invalidates automatically. Do not build resource timestamps or
template dependency lists in individual routes. `pdf_send_download()` sanitizes
the filename and sends private, non-browser-cacheable PDF download headers.

---

## Utilities

### `[#include file=path#]`
Includes a file server-side. The path must resolve inside the project; anything else prints nothing.

```
[#include file=[#base-path#]ext/tpl/head-scripts/index.tpl#]
```

UUID, salt, slug, MD5, resource-key normalization, and plain-text stripping are PHP helpers in `core/lib/util.php`, not template shortcodes. Use them from PHP libraries, route files, install scripts, and CLI scripts:

```php
load_library('util');

$uuid = generate_uuid();
$salt = generate_salt();
$slug = make_slug($title);
$hash = md5_uuid($slug);
$text = plain_text($html);
```

### `[#env KEY default=value#]`
Returns a value from `.env`, falling back to `default` when the key is missing. The settings core keeps secret (`PEPPER`, `STATS_KEY`, `ADMIN_PASSWORD`, `SMTP_PASSWORD`, `AGENT_REMOTE_PASSWORD`, `OPENAI_API_KEY`, `RESEND_API_KEY`, `GIT_TOKEN`) are never printed: read those in PHP with `env()`.

```
[#env STRIPE_LINK_ANNUAL#]
[#env MAIL_SERVICE default=resend#]
```

### `[#logged-in#]`
Returns `"logged-in"` if a user session is active.

```
[#if logged-in=(empty) redirect=login#]
```

### `[#feature-cond features=name#]`
Conditionally renders content based on whether the current user has a specific feature/permission.

```
[#feature-cond features=edit-articles tpl=edit-button#]
[#feature-cond features=view-admin-dashboard echo="<a href='/nb-admin'>Admin</a>"#]
[#feature-cond features=view-admin-dashboard tpl=admin-panel tpl_else=access-denied#]
[#feature-cond features=edit-articles,view-reports tpl=dashboard#]
```

Parameters:
- `features` - comma-separated list of feature names; access is granted if the user has **any** of them, or has `(all)` (admin)
- `tpl` - template to render if access is granted
- `tpl_else` - template to render if access is denied
- `echo` - string to output if access is granted
- `echo_else` - string to output if access is denied

Canonical permissions use `<operation>-<resource_id>`, for example `view-articles`, `create-products`, `edit-.content`, or `delete-users`. Hidden resource ids keep their leading dot in stored permissions. `manage-<resource_id>` grants all standard operations for that one resource. The admin role always has `(all)` which bypasses all feature checks.

Permissions are assigned to roles in `/nb-admin/roles/`. Use the role permissions screen to edit checkbox-based permissions; custom tokens can still be stored for project-specific checks. Legacy tokens such as `manage-content`, `(any)`, `get_<resource>`, `add-<resource>`, and `(any)_<resource>` are normalized during session loading and upgrade migration, but new code should use canonical permissions.

### `[#date input fmt=Y-m-d#]`
Formats a date value. Input can be a Unix timestamp, a date string, or omitted (defaults to today). Use `fmt=full`, `long`, `medium`, or `short` for locale-aware human-readable output. Any other `fmt` value uses PHP date format syntax for an exact technical format. Pass `lang=` to override the detected language.

```
[#date fmt=d-m-Y#]                        → today in d-m-Y format
[#date item.date fmt="j F Y"#]            → formats item.date
[#date 1741265734 fmt=Y-m-d#]             → formats Unix timestamp
[#date item.date fmt=long#]               → locale-aware long date
```

`[#fmt var=item.date type=date fmt=long#]` uses the same date formatter. Named formats use PHP's `intl` extension when available and fall back to deterministic English PHP formats without failing the page.

### `[#get-first dataset#]`
Stores the first item of a dataset into `first` (or a custom variable).

```
[#get-first data.articles#]
[#get first.title#]

[#get-first data.articles var=latest#]
[#get latest.date#]
```

### `[#last-update#]`
Returns a Unix timestamp of the most recently modified source file across `ext/` and `core/`. Useful with `[#fmt type=ago#]` to display when the site was last updated.

```
[#fmt val=[#last-update#] type=ago#]   → "3 days ago"
[#fmt val=[#last-update#] type=date fmt=d-m-Y#]
```

### `[#obfuscate text#]`
Renders text using invisible Unicode characters and Alpine.js so it's invisible to bots/scrapers but readable by users. Use for email addresses and phone numbers.

```
[#obfuscate info@example.com#]
```

### `[#uri-path#]`
Returns the filesystem path of the current route's directory.

### `[#url-key#]`
Returns a normalized string key for the current URL, useful for body classes or JS page detection.

```
[#set body-classes=[#url-key#]#]
```

`/admin/users/` → `admin_users`, homepage → `_home`.

### `[#http-header type#]`
Outputs a response Content-Type header. Use at the top of non-HTML routes.

```
[#http-header json#]
[#http-header css#]
[#http-header csv#]
```

Types: `css`, `js`, `json`, `woff`, `csv`, `403`, `404`, `500`.

A second parameter also sends long-lived cache headers (`Cache-Control: private`, `Expires`, `Last-Modified`) and answers `304` to a conditional request: `[#http-header css cached#]`.

### `[#system-messages#]`
Renders any queued system messages (set server-side via the session). Use in the HTML template or layout to display flash messages.

### `[#empty-img#]`
Outputs a 1×1 transparent GIF as a data URI. Use as a placeholder `src` before a real image is set.

### `[#max-upload-size#]`
Outputs the PHP `upload_max_filesize` value in human-readable form. Useful in upload form hints.

### `[#unquote varname#]`
Outputs a variable's value with quotes HTML-escaped (`"` → `&quot;`, `'` → `&apos;`). Safe for embedding variable values inside HTML attribute strings.

```html
<div x-data='{"title": "[#unquote item.title#]"}'></div>
```

### `[#collect-script path#]`
Defers a script include to be rendered together at the end of the page, avoiding duplicates. Call without arguments to render all collected scripts.

```
[#collect-script [#base-url#]/ext/static/chart.js#]
...
[#collect-script#]   ← renders all collected scripts here
```

### `[#is-dev-env#]`
Outputs `DEV` or `PROD`. Useful for conditional debug output or environment-specific behaviour.

```
[#if [#is-dev-env#]=DEV tpl=debug-panel#]
```

### `[#detect-language#]`
Returns the active language code. Detection order:
1. URL prefix (`/en/`, `/nl/`)
2. User preference (`?lang=en`)
3. Domain TLD (`.en`, `.nl`)
4. Browser language header
5. Fallback - first language defined in site config

### `[#debug#]`
Outputs debug information. Use during development only.

```
[#debug#]
[#debug variables session#]
```

### `[#log message#]`
Writes to `ext/data/.tmp/logs/system.log`.

### `[#nop#]`
No-op. Use to comment out shortcodes temporarily.

## Shortcode API classification

The inventory below is generated from `*_sc()` implementations in `core/lib` and `core/modules/*/lib` and cross-checked against local `ext/` usage. Public shortcodes are stable for project templates. Internal/admin shortcodes are supported for core templates and modules but should not be treated as project-facing APIs.

| Classification | Shortcodes |
|---|---|
| Public core | `base-path`, `base-url`, `cfield`, `collect-script`, `count`, `data`, `data-count`, `data-join`, `data-last-update`, `data-sort`, `date`, `debug`, `detect-language`, `empty-img`, `env`, `fmt`, `get`, `get-first`, `get-html`, `http-header`, `if`, `include`, `is-url`, `last-update`, `log`, `logged-in`, `max-upload-size`, `module`, `nop`, `obfuscate`, `redirect`, `repeat`, `set`, `system-messages`, `text`, `unquote`, `uri-path`, `url`, `url-key` |
| Public module | `access`, `api-allow`, `build-form`, `feature-cond`, `first-img-uuid`, `form-key`, `get-form-errors`, `get-img-html`, `get-user`, `honeypot-field`, `img-url`, `json2post`, `logout`, `post`, `role-cond`, `role-switch`, `sanitize`, `sticky`, `userfield`, `username`, `validate` |
| Internal/admin | `api-session`, `api-token`, `disk-space-resource`, `exif`, `field-name`, `files`, `files-unused`, `files-usage`, `find`, `get-resource-meta`, `get-resource-record`, `get-resource-records`, `get-system-log`, `get-user-resources`, `json`, `openai-complete`, `openai-translate`, `pages`, `render-field`, `resource-name`, `resources`, `sys-messages`, `thumbnail` |

---

## Additional public interfaces

These notes describe public shortcodes listed in the framework inventory whose usage is not expanded above. Use the dedicated guides for complete workflows.

| Shortcode | Behavior |
|---|---|
| `[#module user forms#]` | Load named modules; normal module discovery often makes explicit loading unnecessary. |
| `[#data-last-update events#]` | Return the latest resource file timestamp, or zero when the resource does not exist. |
| `[#access feature=view-events#]` | Enforce feature access before protected output; access failure redirects to the configured denial address. |
| `[#api-allow method=post resource=applications#]` | Allow and dispatch a narrow resource operation for a route using the framework's public request-key checks; do not use it to bypass protected resource permissions. |
| `[#role-cond roles=editor tpl=editor-tools tpl_else=visitor-tools#]` | Render the selected template when one of the named roles is enabled, otherwise the optional fallback. |
| `[#role-switch tpl=account-panel#]` | Render a template with the first enabled role suffix, or `.anonymous` without a session. |
| `[#get-user var=user#]` | Load the current user's record into a variable; `uuid=` selects a record explicitly. Apply authorization before exposing another user's fields. |
| `[#userfield name#]` | Return one field of the current user; an explicit UUID can select another record and requires appropriate authorization in the calling flow. |
| `[#username#]` | Return the signed-in username, or `anonymous`. |
| `[#logout#]` | Clear the current user session and redirect; `redirect=` selects the destination. |
| `[#form-key contact#]` | Render hidden request-key and form identifier inputs; `plain` returns the key, and `x-init` initializes the form's browser state. |
| `[#honeypot-field#]` | Render the configured hidden honeypot input for a public form. |
| `[#post#]` | Process a keyed POST through the route's validation and submission files. Validation is required before submission handling. |
| `[#get-form-errors email#]` | Load the field's validation messages into `validation_errors`; no argument selects global errors. |
| `[#sticky email default=you@example.com#]` | Recover a form value from submitted or stored state for a rerender. |
| `[#json2post#]` | Convert a posted JSON object into PHP POST values for an existing handler. |
| `[#first-img-uuid var=record.body#]` | Find the first image UUID in rich content. |
| `[#img-url image-reference#]` | Normalize a UUID, path or image URL to an absolute image URL. |

### Validation and sanitization boundaries

The inventory currently includes `validate` and `sanitize`, but their shortcode entry points do not perform validation or sanitization. Do not rely on calling those tags to protect input. Use the actual PHP validation functions and the framework's field and API handling, as described in [Forms](/en/docs/guides/forms/) and [PHP extension points](/en/docs/reference/php/).

`feature-cond` and role-based presentation choose which templates to show. Backend handlers still authorize each protected action.
