- 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
Frequently Asked Questions
When should I use t-field vs t-out for record data?
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.
How do I pass additional context to a t-call sub-template?
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>.
Why does xpath not match my element?
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()).