Reference

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>:

<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:

[#data participants#]
[#repeat stories.participants csv tpl=story-participant-item#]
<!-- 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.

<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.

// 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.

<!-- 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:

[#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:

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:

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:

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.

<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 and PHP extension points.

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

View this page as Markdown