- Snippet XML structure and how to register a building block
- Making zones editable with
oe_structure - Adding snippet options (color pickers, layout selectors)
- Dynamic snippets that load data from Odoo models
- JavaScript snippet initialization
Snippet XML Structure
A snippet consists of two parts: the snippet template itself, and an entry in the snippets panel. Both live in the same XML file, typically views/snippets/:
<!-- 1. The snippet HTML template -->
<template id="s_book_highlight" name="Book Highlight">
<section class="s_book_highlight pt-5 pb-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h2>Featured Book</h2>
<p class="lead">Discover our top pick this month.</p>
<a href="/books" class="btn btn-primary">Browse Library</a>
</div>
<div class="col-lg-6">
<img src="/web/image/library.book/1/cover_image"
class="img-fluid rounded" alt="Featured Book"/>
</div>
</div>
</div>
</section>
</template>
<!-- 2. Register it in the snippets panel -->
<template id="snippets" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_content']" position="before">
<t t-snippet="my_module.s_book_highlight"
t-thumbnail="/my_module/static/description/book_highlight_thumb.png"/>
</xpath>
</template>
The thumbnail image (160×120px recommended) shows in the editor's block palette. Store it in your module's static/description/ folder.
oe_structure – Editable Zones
Areas where editors can drag more snippets are marked with the oe_structure class. Add an id attribute to persist changes to the database:
<section class="s_book_highlight">
<div class="container">
<!-- Editable zone: editors can drag snippets here -->
<div class="oe_structure oe_empty" id="oe_structure_book_highlight_1"/>
</div>
</section>
The oe_empty class shows a placeholder message ("Drag building blocks here") when the zone is empty. When an editor drops a snippet here and saves, Odoo stores the HTML in an ir.ui.view record keyed to this id.
Snippet Options
Options let editors configure snippet appearance from the sidebar without editing HTML. Register options by targeting the snippet's CSS class:
<template id="s_book_highlight_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="BookHighlight" data-selector=".s_book_highlight">
<!-- Background style option -->
<we-select string="Background">
<we-button data-select-class="bg-light">Light</we-button>
<we-button data-select-class="bg-dark text-white">Dark</we-button>
<we-button data-select-class="bg-primary text-white">Primary</we-button>
</we-select>
<!-- Layout option -->
<we-select string="Layout">
<we-button data-select-class="flex-row">Image Right</we-button>
<we-button data-select-class="flex-row-reverse">Image Left</we-button>
</we-select>
</div>
</xpath>
</template>
data-select-class adds/removes CSS classes on the snippet root element when an option is selected. No JavaScript needed for class-based options.
Dynamic Snippets
Dynamic snippets fetch data from Odoo models and render it in a carousel, grid, or list. They use a JSON route to reload data based on options the editor selects:
class LibrarySnippets(http.Controller):
@http.route('/books/snippet/featured',
type='json', auth='public', website=True)
def featured_books(self, limit=4, **kw):
books = request.env['library.book'].sudo().search([
('website_published', '=', True),
('featured', '=', True),
], limit=limit)
return request.env['ir.ui.view']._render_template(
'my_module.s_featured_books_content',
{'books': books},
)
The snippet's JavaScript calls this route when it initializes and when editor options change. Built-in dynamic snippets like s_dynamic_snippet follow this same pattern.
JavaScript Snippet Initialization
For client-side behavior (carousels, counters, animations), add a JavaScript file in static/src/js/ and register a snippet widget:
/** @odoo-module **/
import { SnippetOption } from '@web_editor/js/editor/snippets.options';
import { registry } from "@web/core/registry";
// Public widget — runs on the frontend for visitors
import publicWidget from '@web/legacy/js/public/public_widget';
publicWidget.registry.BookHighlight = publicWidget.Widget.extend({
selector: '.s_book_highlight',
start() {
// Runs when snippet loads on the page
this._initCounter();
return this._super(...arguments);
},
_initCounter() {
const counter = this.el.querySelector('.book-count');
if (counter) {
// Animate the counter
const target = parseInt(counter.dataset.target, 10);
counter.textContent = target.toLocaleString();
}
},
});
- A snippet needs two XML records: the template itself and an entry in
website.snippets - Use
oe_structurewith a uniqueidto create editable zones within your snippet - Snippet options use
we-select/we-buttonwithdata-select-classfor class-based configuration - Dynamic snippets fetch model data via JSON routes and re-render when options change
Frequently Asked Questions
How do I prevent editors from moving or deleting my snippet?
Add data-oe-protected="true" to the snippet root element. This prevents the website editor from moving, duplicating, or removing it. Useful for structural snippets that are part of a page template rather than content blocks.
Where does Odoo store edits made to a page in the editor?
Each page is an ir.ui.view record with a unique key. When an editor saves, Odoo creates a "cow" (copy-on-write) view that overrides the original template for the current website. The original template is preserved and the override is stored as a child view with website_id set.
Can I make snippet text content editable inline?
Yes — add data-oe-model, data-oe-id, and data-oe-field attributes to an element. Odoo's editor will allow inline editing and save changes back to the model field. For static text (not linked to a model), just add the oe_editable class to enable inline text editing that saves to the view record.