Skip to content

Twig Overview

Total CMS uses Twig as its templating engine, providing a powerful, secure, and designer-friendly way to create dynamic templates. This guide covers how Twig is integrated into Total CMS and how to use it effectively.

Twig is a modern template engine for PHP that offers:

  • Clean syntax - Easy to read and write
  • Secure - Automatic output escaping
  • Fast - Compiled templates with caching
  • Flexible - Extensible with custom functions and filters
  • Designer-friendly - No PHP knowledge required

Display variables using double curly braces:

{{ variable }}
{{ user.name }}
{{ product['price'] }}
{{ items[0] }}
{# This is a comment and won't appear in the output #}

Control structures use curly braces with percent signs:

{% if cms.auth.userLoggedIn() %}
Welcome, {{ cms.auth.userData().name }}!
{% endif %}
{% for item in items %}
<li>{{ item.title }}</li>
{% endfor %}

The most important Twig variable in Total CMS is cms, which fetches content from the CMS:

{# Get all items from a collection #}
{% set posts = cms.collection.objects('blog') %}
{# Get a specific item by ID #}
{% set coolguy = cms.collection.object('users', 'joeworkman') %}

For more information, check out the Total CMS Content with Twig docs.

Total CMS ships stylesheets and scripts that its own Twig output depends on — grid layout, galleries, pagination, icons, and the JavaScript that turns mailto addresses into real links. Two helpers emit them, and every layout you write should call both:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
{{ cms.assetsHead() }}
{# your own stylesheet goes after, so your rules win #}
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
{% block content %}{% endblock %}
{{ cms.assetsBody() }}
</body>
</html>

cms.assetsHead() emits the core stylesheets and preload hints. cms.assetsBody() emits the core scripts, which belong at the end of the body. Both also render any assets registered by extensions, so an extension that ships CSS or JS works without you changing your layout.

Order matters in the head: put cms.assetsHead() before your own stylesheet so your rules override the defaults rather than the other way round.

Nothing errors. The page renders, the markup is correct, and things quietly look or behave wrong:

Missing assetSymptom
cms-grid.css{% cmsgrid %} renders as an unstyled stack
gallery.css / gallery.jsgalleries don’t lay out, lightbox never opens
pagination.cssLoad More controls render unstyled
content.css / icons.cssdefault content styles and icons are missing
content.jsmailto addresses stay inert text
htmx.min.jsLoad More and other interactive fragments never fetch

Because the symptom is “my grid looks wrong” rather than an error, this is worth checking first when a template misbehaves for no apparent reason.

Don’t hardcode these files yourself. The list changes between releases, and the helpers handle ordering, module type, preloading, and cache-busting for you.

{% set posts = cms.collection.objects('blog') %}
<div class="blog-posts">
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<time>{{ post.date | date('F j, Y') }}</time>
<div class="content">
{{ post.content | markdown }}
</div>
{% if post.tags %}
<div class="tags">
{% for tag in post.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% set gallery = cms.render.gallery('portfolio') %}
<div class="gallery">
<h2>{{ gallery.title }}</h2>
<div class="grid">
{% for image in gallery.images %}
<figure>
<img src="{{ cms.render.image(image.id, {w: 400, h: 300}) }}"
alt="{{ image.alt }}"
loading="lazy">
<figcaption>{{ image.caption }}</figcaption>
</figure>
{% endfor %}
</div>
</div>
{% set feature = api('toggle', 'show-newsletter') %}
{% if cms.toggle('feature') %}
<div class="newsletter">
{% include 'partials/newsletter-signup.twig' %}
</div>
{% endif %}

Total CMS adds many custom filters to Twig:

{{ text | markdown }} {# Convert markdown to HTML #}
{{ text | nl2br }} {# Convert newlines to <br> #}
{{ text | truncate(100) }} {# Truncate to 100 characters #}
{{ text | title }} {# Title Case Text #}
{{ post.date | date('F j, Y') }} {# January 15, 2024 #}
{{ post.date | date('Y-m-d') }} {# 2024-01-15 #}
{{ post.date | dateRelative }} {# 2 hours ago #}
{{ post.date | date_modify('+1 day') }} {# Add one day #}
{{ items | first }} {# Get first item #}
{{ items | last }} {# Get last item #}
{{ items | random }} {# Get random item #}
{{ items | slice(0, 3) }} {# Get first 3 items #}
{{ items | filter(v => v.active) }} {# Filter active items #}
{{ items | map(v => v.name) }} {# Extract names #}
{{ random(1, 100) }} {# Random number #}
{{ "now" | date('Y') }} {# Current year #}
{{ dump(variable) }} {# Debug variable #}

Total CMS automatically caches compiled templates. Clear cache when making large changes.

Go to Cache Manager

Twig will automatically render HTML. If you want to display the raw HTML stored in the CMS you can.

{{ html_content | e }}

Access global variables in all templates:

{{ cms.api }}
{{ cms.dashboard }}
{{ cms.logout }}
{{ cms.domain }} {# HTTP host #}
{{ cms.siteName }} {# Site display name — operator-set, falls back to domain #}

Enable debug mode to use the dump() function:

{{ dump() }} {# Dump all variables #}
{{ dump(user) }} {# Dump specific variable #}
{% if post is defined %}
{{ post.title }}
{% endif %}
{# Or use default filter #}
{{ post.title | default('Untitled') }}
{% try %}
{{ risky_operation() }}
{% catch %}
<p>An error occurred</p>
{% endtry %}
<script>
window.appData = {
user: {{ user | json_encode }},
apiUrl: "{{ api_url }}",
csrfToken: "{{ csrf_token() }}"
};
</script>
<script>
const items = [
{% for item in items %}
{
id: {{ item.id }},
name: "{{ item.name | e('js') }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
];
</script>

Remember: Twig makes your templates more maintainable, secure, and easier to work with. Take advantage of its features to create clean, efficient templates!