Ad – 728×90
💻 Website Development

QWeb Templates for Website Pages

QWeb is Odoo's XML-based templating engine. Website pages use QWeb templates stored as ir.ui.view records. Templates inherit from website.layout, use t- directives for logic, and can be extended at specific points using xpath expressions.

⏱️ 25 min 🎯 Intermediate 📅 Updated 2026
What you'll learn:
  • QWeb template structure and how to register templates in XML
  • Using website.layout as the base wrapper
  • Core t- directives: t-if, t-foreach, t-call, t-set
  • Difference between t-field, t-out, and t-esc
  • Template inheritance with xpath

Registering a Template

Templates live in your module's views/ directory and are loaded via __manifest__.py:

XML
<!-- 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:

SlotPurpose
titlePage <title> tag content
meta_descriptionMeta description override
headAdditional <head> content (CSS, meta tags)
website_form_object_modelUsed by website forms for SPAM protection

Core t- Directives

t-if / t-elif / t-else — conditional rendering:

XML (QWeb)
<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:

XML (QWeb)
<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:

XML (QWeb)
<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

DirectivePurposeEscaping
t-fieldRender Odoo field with widget formatting; also enables inline editing by website publisherYes (handled by widget)
t-outRender any Python expression; HTML is rendered as-is (use for trusted HTML)No — HTML rendered raw
t-escRender any Python expression, HTML-escaped (plain text)Yes — safe for user input
XML (QWeb)
<!-- 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"/>
Ad – 728×90

Template Inheritance with xpath

Extend an existing template without copying it by using inherit_id and xpath:

XML
<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

XML (QWeb)
<!-- 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>
Key takeaways:
  • Always wrap website page templates with t-call="website.layout"
  • Use t-field for Odoo model fields — it enables inline editing by website publishers
  • Use t-esc for user-supplied strings; t-out only for trusted HTML
  • Extend templates with inherit_id + xpath instead 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()).