QWeb Templates for the Website
Website pages are QWeb templates — XML with t- directives — rendered server-side into HTML. The same QWeb engine powers reports and the portal, so learning it once pays off across Odoo.
<template id="product_page" name="Product Page">
<t t-call="website.layout"> <!-- reuse site header/footer -->
<div class="container">
<h1 t-field="product.name"/> <!-- t-field = formatted value -->
<p t-esc="product.description"/> <!-- t-esc = escaped text -->
<t t-if="product.in_stock">In stock</t>
</div>
</t>
</template>
| Directive | Does |
|---|---|
t-call="website.layout" | wrap in the site's theme/shell |
t-field | output a value with its formatting |
t-esc | output escaped text |
t-foreach / t-if | loop / condition |
Server-side rendering: unlike a client framework, website QWeb runs on the server — the controller queries data, passes it to the template, and Odoo returns finished HTML. This is good for SEO (crawlers see real content) and works without JavaScript. t-call="website.layout" is the key habit: it wraps your content in the site's header, footer, menus, and theme, so custom pages look native automatically — never build a full page from scratch. t-field vs t-esc: use t-field for record values that need formatting (prices with currency, dates in locale, rich-text HTML fields) — it also enables in-place editing in the website editor; use t-esc for plain escaped text. Security: prefer t-esc/t-field (escaped) over t-out (raw HTML) for any user-supplied data, or you risk XSS on a public page. Data comes from the controller via request.render(template, {values}). Extend, don't copy: customize existing templates with xpath inheritance so Odoo upgrades don't overwrite your changes.
🏋️ Practical Exercise
- Create a frontend QWeb template.
- Reuse one with
t-call. - Output data with
t-field/t-esc. - Loop with
t-foreach. - Inherit a template with
xpath.
🔥 Challenge Exercise
Build a frontend QWeb template that renders a list of records passed from a controller, using t-foreach and t-esc, and inherit an existing template with xpath. Explain the difference between frontend QWeb (website) and report QWeb (PDF).
- QWeb template structure and how to register templates in XML
- Using
website.layoutas the base wrapper - Core
t-directives:t-if,t-foreach,t-call,t-set - Difference between
t-field,t-out, andt-esc - Template inheritance with
xpath
Registering a Template
Templates live in your module's views/ directory and are loaded via __manifest__.py:
<!-- views/website_books_templates.xml -->
<odoo>
<template id="website_books_listing" name="Books Listing">
<t t-call="website.layout">
<t t-set="title">Our Library</t>
<div class="container">
<h1>Books</h1>
<div class="row">
<t t-foreach="books" t-as="book">
<div class="col-md-4">
<h3><t t-out="book.name"/></h3>
<p><t t-out="book.author"/></p>
</div>
</t>
</div>
</div>
</t>
</template>
</odoo>
The template key is my_module.website_books_listing. In your controller: request.render('my_module.website_books_listing', {'books': books}).
website.layout – The Base Wrapper
t-call="website.layout" wraps your content with the full website page structure: header, footer, theme CSS/JS, language selector, and edit toolbar (when logged in as publisher). Always use it for website pages.
Named slots you can fill via t-set:
| Slot | Purpose |
|---|---|
| title | Page <title> tag content |
| meta_description | Meta description override |
| head | Additional <head> content (CSS, meta tags) |
| website_form_object_model | Used by website forms for SPAM protection |
Core t- Directives
t-if / t-elif / t-else — conditional rendering:
<t t-if="book.available_copies > 0">
<span class="badge bg-success">Available</span>
</t>
<t t-else="">
<span class="badge bg-danger">Unavailable</span>
</t>
t-foreach / t-as — loops:
<t t-foreach="books" t-as="book">
<!-- book_index: 0-based index -->
<!-- book_first: True on first iteration -->
<!-- book_last: True on last iteration -->
<div t-attf-class="book-card #{book_first and 'first' or ''}">
<t t-out="book.name"/>
</div>
</t>
t-set — define a variable:
<t t-set="page_title">Library – <t t-out="book.name"/></t>
<title><t t-out="page_title"/></title>
t-field vs t-out vs t-esc
| Directive | Purpose | Escaping |
|---|---|---|
| t-field | Render Odoo field with widget formatting; also enables inline editing by website publisher | Yes (handled by widget) |
| t-out | Render any Python expression; HTML is rendered as-is (use for trusted HTML) | No — HTML rendered raw |
| t-esc | Render any Python expression, HTML-escaped (plain text) | Yes — safe for user input |
<!-- Renders field with Odoo formatting (date, currency, etc.) -->
<span t-field="book.publish_date"/>
<!-- Renders HTML content from a fields.Html field -->
<div t-out="book.description"/>
<!-- Safe for user-supplied strings (escapes <>&) -->
<span t-esc="user_input"/>
Template Inheritance with xpath
Extend an existing template without copying it by using inherit_id and xpath:
<template id="website_books_listing_extend"
inherit_id="my_module.website_books_listing"
name="Books – Add Category Filter">
<!-- Insert before the h1 -->
<xpath expr="//h1" position="before">
<div class="category-filter">
<a href="/books">All</a>
<t t-foreach="categories" t-as="cat">
<a t-attf-href="/books?category=#{cat.id}">
<t t-out="cat.name"/>
</a>
</t>
</div>
</xpath>
<!-- Replace a specific element -->
<xpath expr="//div[@class='row']" position="replace">
<div class="row g-4">...</div>
</xpath>
</template>
position values: before, after, inside (appends children), replace, attributes.
Dynamic Attributes – t-att and t-attf
<!-- t-att-{name}: Python expression -->
<a t-att-href="'/books/' + slug(book)"><t t-out="book.name"/></a>
<!-- t-attf-{name}: f-string style with #{} interpolation -->
<div t-attf-class="book-card #{book.available and 'available' or 'unavailable'}">
...
</div>
- Always wrap website page templates with
t-call="website.layout" - Use
t-fieldfor Odoo model fields — it enables inline editing by website publishers - Use
t-escfor user-supplied strings;t-outonly for trusted HTML - Extend templates with
inherit_id+xpathinstead of copying
Interview Questions
- What is QWeb on the frontend?
- What does
t-calldo? - How do you loop in a QWeb template?
- How do you inherit a website template?
- How does frontend QWeb differ from report QWeb?
Related Topics
FAQ
Use t-field when you want Odoo's built-in formatting (currency symbols, date formatting, many2one display names) and when you want website publishers to edit content inline. Use t-out when you are rendering raw HTML from a fields.Html field or building a custom string in the controller. Never use t-out with user-supplied text — use t-esc instead.
Use t-set inside the t-call block. Variables set this way are scoped to the called template: <t t-call="my_module.sub_template"><t t-set="extra_var" t-value="42"/></t>.
XPath expressions must match exactly. Common issues: class attribute order matters in XPath ([@class='foo bar'] won't match class="bar foo"), so prefer matching by id or t-attf-id. Use //div[hasclass('my-class')] for class-based selection (Odoo extends XPath with hasclass()).

