Twig Basics
Thelia 3 uses Twig as the template engine for front-office development.
For complete Twig documentation, see twig.symfony.com.
Template inheritance
Base layout
All pages extend a base layout:
{# base.html.twig #}
<!DOCTYPE html>
<html lang="{{ lang_code }}">
<head>
{# The title, meta and canonical tags are rendered by whoever answers this hook #}
{{ theme_hook('layout.head.top', {
title: block('title') is defined ? block('title')|trim : null,
}) }}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}" blocking="render">
{% endblock %}
{% block javascripts %}{{ importmap('app') }}{% endblock %}
</head>
<body>
{% block header %}<twig:Layouts:Header:Base />{% endblock %}
<main>{% block body %}{% endblock %}</main>
{% block footer %}<twig:Layouts:Footer:Base />{% endblock %}
</body>
</html>
Extending the base layout
{# product.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}
{{ product.i18ns.title }} - {{ parent() }}
{% endblock %}
{% block body %}
<h1>{{ product.i18ns.title }}</h1>
<p>{{ product.i18ns.description|raw }}</p>
{% endblock %}
Data access
Use the resources() function to fetch data from the API:
{# Get product by ID #}
{% set product = resources('/api/front/products/' ~ productId) %}
{# Get products in category #}
{% set products = resources('/api/front/products', {
'productCategories.category.id': categoryId,
'visible': true,
'itemsPerPage': 20
}) %}
{% for product in products %}
<div>{{ product.i18ns.title }}</div>
{% endfor %}
See Data Access for complete documentation.
Thelia-specific functions
URL attributes
Get URL parameters using attr():
{% set productId = attr('product', 'id') %}
{% set categoryId = attr('category', 'id') %}
SEO tags
A theme does not render its SEO tags itself. It opens a hook in the <head> and passes what the
current page knows:
{{ theme_hook('layout.head.top', {
breadcrumb,
title: block('title') is defined ? block('title')|trim : null,
description: block('meta_description') is defined ? block('meta_description')|trim : null,
og_type: block('og_type') is defined ? block('og_type')|trim : null,
}) }}
The SEOne module answers that hook and renders the title, the meta description, the canonical link, the hreflang tags and the breadcrumb JSON-LD, falling back to its own values for anything the page left unset. See Theme hooks.
Its Twig functions (SEOneBreadcrumb, SEOnePageH1, SEOnePageCanonical, SEOneWebSite,
SEOneWebPage, SEOneLocalBusiness, ...) stay available for the values a page wants to read
directly:
<h1>{{ SEOnePageH1()|default(null) ?: attr('product', 'title') }}</h1>
The SEOne* functions are not core Twig functions. They come from the SEOne module
(thelia/seone-module), which Flexy declares as a dependency. A theme built without that module
renders the head through the hook and simply gets nothing back.
Translation
{{ 'Add to cart'|trans }}
{{ 'Welcome, %name%!'|trans({'%name%': customer.firstname}) }}
{{ '%count% item|%count% items'|trans({'%count%': cart.itemCount}) }}
Price formatting
{{ price|format_currency('EUR', locale: lang_code) }}
Components
Twig components
Components are named after their class path under components/, with no prefix. The tag syntax is
what Flexy uses everywhere; : prefixes an attribute whose value is a Twig expression rather than a
string:
<twig:Organisms:ProductCard:Base :product="product" />
<twig:Molecules:Accordion:Base id="product-details" multiple>
<twig:Molecules:Accordion:Item value="description" open>
{# ... #}
</twig:Molecules:Accordion:Item>
</twig:Molecules:Accordion:Base>
LiveComponents
<twig:Layouts:ProductListing:Base :categoryId="categoryId" />
See LiveComponents for details.
Asset management
Assets are served by AssetMapper, and paths resolve inside the theme's assets/ directory:
{# CSS #}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}" blocking="render">
{# JavaScript #}
{{ importmap('app') }}
{# Single asset #}
{{ asset('images/logo.png') }}
Stimulus controllers
<div {{ stimulus_controller('drawer') }}>
<button {{ stimulus_action('drawer', 'toggle') }}>Toggle</button>
<div {{ stimulus_target('drawer', 'panel') }}>Content</div>
</div>
Debugging
{{ dump(product) }}
{{ dump() }} {# All variables #}
dump() only works when APP_DEBUG=1.
Best practices
Use DataAccessService
{# Recommended #}
{% set products = resources('/api/front/products', {visible: true}) %}
Keep templates clean
{# Good - logic in component #}
<twig:Organisms:ProductCard:Base :product="product" />
{# Avoid - complex business logic in template #}
{% if someComplexCondition and anotherCondition %}
...
{% endif %}
Escape user content
{{ userComment }} {# Auto-escaped #}
{{ product.i18ns.description|raw }} {# Trusted admin content only #}
Next steps
- Data Access - Learn the
resources()function - LiveComponents - Build reactive components
- Flexy Theme - Explore the default theme