Twig Basics
Thelia 3 uses Twig as the template engine for front-office development.
Official Documentation
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>
<title>{% block title %}{{ SEOnePageTitle() }}{% endblock %}</title>
{% block stylesheets %}{{ encore_entry_link_tags('app') }}{% endblock %}
{% block javascripts %}{{ encore_entry_script_tags('app') }}{% endblock %}
</head>
<body>
{% block header %}{{ include('@components/Layout/Header.html.twig') }}{% endblock %}
<main>{% block body %}{% endblock %}</main>
{% block footer %}{{ include('@components/Layout/Footer.html.twig') }}{% 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 functions
<title>{{ SEOnePageTitle() }}</title>
<meta name="description" content="{{ SEOnePageDesc() }}">
<link rel="canonical" href="{{ SEOnePageCanonical() }}">
{{ SEOneBreadcrumbJsonLd(breadcrumb)|raw }}
{{ SEOneHreflang()|raw }}
note
The SEOne* functions (SEOnePageTitle, SEOnePageDesc, SEOnePageCanonical, SEOneBreadcrumbJsonLd, SEOneHreflang, SEOneBreadcrumb) are not core Twig functions. They are provided by the SEOne module (thelia/seone-module), which Flexy declares as a dependency. A theme built from scratch without that module would not have these functions available.
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
{{ component('Flexy:ProductCard', {product: product}) }}
{% component 'Flexy:Card' %}
{% block header %}Card Title{% endblock %}
{% block content %}Card content{% endblock %}
{% endcomponent %}
LiveComponents
{{ component('Flexy:CategoryFilters', {
initialCategoryId: categoryId,
initialPage: 1
}) }}
See LiveComponents for details.
Asset management
{# CSS #}
{{ encore_entry_link_tags('app') }}
{# JavaScript #}
{{ encore_entry_script_tags('app') }}
{# Single asset #}
{{ asset('build/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 #}
tip
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 #}
{{ component('Flexy:ProductPrice', {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