Pseudo-tags
A pseudo-tag is a function called like a template tag that returns a ready piece of HTML:
{icon 'edit' class='icon-sm'}Handy for repeated fragments — icons, buttons, badges — that do not deserve a chunk of their own.
No parentheses, and never inside an expression
This is a tag, not a function, and it takes tag syntax: parameters separated by spaces, no parentheses, no commas. Anything else is a compile error, Function icon not found — that is a blank page and a 500:
{icon 'edit' class='icon-sm'} {* correct *}
{icon('edit')} {* 500 *}
{set $i = icon('edit')} {* 500 *}
{if icon('edit')} {* 500 *}If you need the value in a variable or a condition, write a modifier instead: it works as a filter, as a function and inside expressions.
📁 Where to declare them
core/App/Helpers/fenom/inline_tags.phpThe file returns a name → function map. The first argument is the parameter array: unnamed parameters land under numeric keys, named ones under their own.
return [
'icon' => function ($params) {
// {icon 'edit' class='icon-sm'} → ['edit', 'class' => 'icon-sm']
$name = array_shift($params);
$attrs = [];
foreach ($params as $key => $value) {
$attrs[] = htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
}
$attrStr = $attrs ? ' ' . implode(' ', $attrs) : '';
$path = MODX_BASE_PATH . "assets/icons/{$name}.svg";
if (!file_exists($path)) {
return "<!-- icon '{$name}' not found -->";
}
$svg = file_get_contents($path);
return preg_replace('/<svg\b(.*?)>/', '<svg$1' . $attrStr . '>', $svg, 1);
},
];{icon 'edit' class='icon-sm text-muted'}
{icon 'user' class='icon-lg' aria-hidden='true'}As with modifiers, $this->modx is available — the file is included from inside View.
🔧 Built-in pseudo-tags
The component registers its own; you do not declare these:
| Tag | Returns |
|---|---|
{csrf} | <input type="hidden" name="_token" …> |
{csrf_token} | the token value alone |
{meta_csrf} | <meta name="csrf-token" …> for fetch requests |
{spam} | a hidden honeypot field |
{recaptcha 'login'} | a reCAPTCHA v3 field with the action and public key |
{route 'home'} | the URL of a registered route |
{lang 'auth.title'} | a translation string |
{pb_filters table='Products'} | a ready-made table filter form |
{heroicon $block.icon style='solid'} | an inlined Heroicon |
{pbList parent=$id tpl='item' limit=10} | a resource lister with AJAX pagination |
route and lang also exist as modifiers, where they do more (substitutions, an explicit language) and work inside expressions: {$item.uri|lang_url}, {route('order', ['id' => $id])}.
The callback contract
pbFenom keeps the historical addFunction() contract: the callback receives ($params, $tpl, $var), where $params is the parsed parameter array. That is exactly how inline_tags.php is wired up.
If you want the function's own signature to be the template API, register it with addFunctionSmart(). There is no dedicated hook file for that: View::init() puts the engine instance into modx()->fenom, so take it from there — from a plugin on OnLoadWebDocument, for instance, or from a snippet:
modx()->fenom->addFunctionSmart('excerpt', function (string $text, int $length = 100) {
return mb_substr(strip_tags($text), 0, $length);
});{excerpt text=$page.introtext length=200}In this fork addFunctionSmart() accepts any callable — a closure, an array callable, an object with __invoke; upstream only ever handled strings. A missing required argument is reported at compile time ("Function excerpt requires the 'text' argument") instead of blowing up mid-render.