Guide 02 · Developer docs

Template languages: Smarty and Twig

Core dna renders server-side sites with Smarty or with Twig, one engine per site, and treats the two as peers. This guide is the one place both are documented: what they share, each engine's language and the platform's additions and limits, the two side by side, and translation.

L1–L2
Unit 02 · Smarty

What the platform adds to Smarty

Core dna registers its own tags and modifiers on top of stock Smarty: four translation forms, |format for dates, {json}, {debug_vars}, {error}, and four modifiers. What each one takes, what it prints, the input that makes it fail, and when to reach for it.

Who this is for. The developer on a Smarty site who meets a tag or modifier that is not in the Smarty manual, or wants to know which platform helper fits a job. Afterwards you can use each one correctly and recognise its failure.

The list

NameKindDoes
{_}FunctionTranslates a key: {_ key='cart.add' domain='site-shop'}
|_ModifierTranslates a key: {'cart.add'|_:'site-shop'}
{__}BlockTranslates the enclosed text, used as the key
|formatModifierFormats a date in the site's date or date-and-time format
{json}BlockTurns the enclosed text into compact JSON, forgiving a trailing comma before }
{debug_vars}FunctionShows the values you pass in the debug panel, on development environments only
{error}FunctionStops the render with an error
|json_encode, |intval, |strvalModifiersPHP's functions of the same names
|base64url_decodeModifierDecodes URL-safe base64
|webix_tableModifierSerialises records for the admin's data tables

All of them pass the security policy, which restricts only PHP functions (The template security policy).

The translation forms

Four ways to look up the same translation files. The modifier with a domain is the one to reach for: it is the shortest, and naming the domain every time avoids the platform's default domain, admin, which is the admin interface's.

Template code Smarty only
{'cart.button.add'|_:'site-shop'}                                {* the usual form *}
{'cart.items'|_:'site-shop':['count' => $itemCount]}             {* with parameters *}
{_ key="product.`$code`.label" domain='site-shop'}               {* a key built from a variable *}
{__ domain='site-shop'}checkout.terms.full{/__}                  {* the enclosed text is the key *}

The function needs key=: {_ 'cart.button.add'} does not compile (too many shorthand attributes). The block uses admin when it has no domain=, even after a template has set another default. Files, domains, parameters and the lookup order are in Translating in Smarty.

|format: dates in the site's format

|format formats a date with the format chosen under the site's Internationalization settings: Date format (Globals.i18n.dateFormat, by default d/m/Y) or Date/Time format (Globals.i18n.datetimeFormat, by default d/m/Y H:i). Without an argument it uses the date-and-time format; |format:'date' uses the date format.

Template code Smarty only
{$page->getPublished()|format:'date'}           {* 19/10/2026 with the default setting *}
{$post->getModified()|format}                   {* 19/10/2026 14:05 *}
{$component->getAreaDate('eventDate')|format:'date'}

{$page->getPublished()|date_format:'j F Y'}     {* a format of your own: 19 October 2026 *}
  • It formats dates only. It takes a date from a record's getter, a date from getAreaDate(), or a Date or Datetime area's content object. An empty value prints nothing. Anything else, such as a number, a price or a date written as text, stops the render with an error. It has no locale: the formats are numeric, so nothing needs translating.
  • Dates arrive in the site's timezone. Records convert the dates they return, so the value printed is local time.
  • For numbers and money use |number_format:2 or the number_format() function; for any other date layout, Smarty's |date_format, which takes PHP's date() letters.
  • A date area read with the shorthand, {$component->eventDate}, already prints in the site's format, and |format cannot take it. Use getAreaDate() when you need to format it.

{json} and |json_encode

Both produce JSON, from opposite directions. |json_encode encodes a value you have built as an array; {json} takes JSON you have written out as text, with template tags inside it, decodes it and encodes it again compactly. Its one concession is a trailing comma before a closing }, which it removes, so a {foreach} can write a comma after every item of an object.

Template code Smarty only
{* {json}: JSON written out, with a space after each brace so Smarty leaves it alone *}
<script type="application/ld+json">
{json}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {foreach $page->getAllParents() as $parent}
    { "@type": "ListItem", "position": {$parent@iteration}, "name": {$parent->getName()|json_encode} }{if !$parent@last},{/if}
    {/foreach}
  ],
}
{/json}
</script>

{* |json_encode: the same, built as an array and encoded once *}
{$items = []}
{foreach $page->getAllParents() as $parent}
  {$items[] = ['@type' => 'ListItem', 'position' => $parent@iteration, 'name' => $parent->getName()]}
{/foreach}
<script type="application/ld+json">{['@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $items]|json_encode}</script>

Prefer |json_encode. {json} decodes what you write and re-encodes it compactly: JSON it cannot decode, including a trailing comma before ], comes out as the word null; an empty object comes out as []; and every value written into it still needs its own |json_encode, since a quote in a page name has to be escaped there too. Building an array and encoding it once cannot produce invalid JSON.

{debug_vars} and {error}

{debug_vars} takes any named values and shows them, dumped in full, in the debug panel the platform adds at the foot of the page. It works only on a development environment; anywhere else it does nothing and prints nothing, so a forgotten one is harmless. The panel needs the page to have a </body>.

{error} stops the render on purpose. With message= it raises that message; with exception= it raises an exception you hold; with neither, An error occurred rendering the template. A development environment shows the stack trace with your message; production shows the site's error page and logs the message.

Template code Smarty only
{debug_vars page=$page sections=$page->getAreaCollectionElements('pageSections')}

{if !$component->getAreaImage('image')}
  {error message="Hero component `$component->getId()` has no image"}
{/if}

Use {error} for a state that should never reach a visitor and is cheaper to fix than to render around. For content an editor may legitimately leave empty, render nothing instead.

The remaining modifiers

  • |json_encode: a value handed to JavaScript or written into JSON, such as a job's request body. <script nonce="{$_inlineScriptNonce}">window.siteConfig = {$config|json_encode};</script>.
  • |intval: a number stored as text, such as a Number or Select area, before arithmetic or a comparison: {if ($component->getAreaText('columns')|intval) > 2}.
  • |strval: the reverse, a number compared with a string key.
  • |base64url_decode: a value passed in a URL as URL-safe base64, where - and _ stand for + and / and padding may be missing.
  • |webix_table: turns a record or a list of records into rows for the admin's Webix data tables. It belongs to admin templates and stops the render for anything that is not a record.

One modifier the policy allows that you should never commit is |dd, which dumps its value and ends the request with a 500 on every environment (The PHP functions allowed as modifiers).

Go deeper

Was this page helpful?

Your answer goes to the docs team.