- How to write a website controller with
website=True - Using
request.websiteandrequest.envin website context - Slugified URL patterns for SEO-friendly record pages
- Handling 404 and unpublished records
- Redirecting to canonical URLs
Basic Website Controller
A website controller inherits from odoo.http.Controller and uses website=True:
from odoo import http
from odoo.http import request
class LibraryWebsite(http.Controller):
@http.route('/books', type='http', auth='public', website=True)
def book_listing(self, **kw):
books = request.env['library.book'].search([
('website_published', '=', True),
('website_id', 'in', [False, request.website.id]),
])
return request.render('my_module.website_books_listing', {
'books': books,
})
Key differences from a plain controller:
website=True— activates website contextauth='public'— accessible without loginrequest.website— the activeir.websiterecord
Slugified URLs
SEO-friendly URLs like /books/clean-code-42 combine a human-readable slug with the record ID. Odoo provides slug() to generate these and unslug() to parse them:
from odoo.addons.http_routing.models.ir_http import slug, unslug
class LibraryWebsite(http.Controller):
@http.route('/books/<path:slug_str>', type='http',
auth='public', website=True)
def book_detail(self, slug_str, **kw):
# slug_str = "clean-code-42"
slug_id = unslug(slug_str)[1] # returns (name_part, id) tuple
if not slug_id:
raise werkzeug.exceptions.NotFound()
book = request.env['library.book'].browse(slug_id)
if not book.exists() or not book.website_published:
raise werkzeug.exceptions.NotFound()
# Redirect to canonical slug in case name changed
expected_slug = slug(book)
if slug_str != expected_slug:
return request.redirect(f'/books/{expected_slug}', 301)
return request.render('my_module.website_book_detail', {
'book': book,
})
slug(record) generates a string like clean-code-42 using record.name and record.id. The 301 redirect to the canonical slug keeps URLs consistent after a name change.
Using request.website
request.website exposes the active website record and its helpers:
website = request.website
# Current website info
website.name # "My Library Website"
website.domain # "library.example.com"
website.company_id # linked res.company
website.default_lang_id # default language
# Useful helpers
website.get_current_pricelist() # website_sale
website.is_publisher() # True if current user can publish pages
website.viewref('my_module.tmpl') # ir.ui.view lookup by key
Handling 404 and Unpublished Records
Always validate existence and publication status before rendering a detail page:
import werkzeug.exceptions
book = request.env['library.book'].browse(book_id)
# Not found
if not book.exists():
raise werkzeug.exceptions.NotFound()
# Unpublished — allow website editors to preview
if not book.website_published and not request.website.is_publisher():
raise werkzeug.exceptions.NotFound()
return request.render('my_module.website_book_detail', {'book': book})
Website editors (users with website.group_website_publisher) should be able to preview unpublished pages, so check request.website.is_publisher() before raising 404.
Extending Existing Website Controllers
Odoo website controllers can be extended using Python class inheritance. Since Odoo uses a registry, subclassing a controller in your module overrides the parent's routes:
from odoo.addons.website.controllers.main import Website as WebsiteMain
class ExtendedWebsite(WebsiteMain):
@http.route('/contactus', type='http', auth='public', website=True)
def contactus(self, **kw):
# Add extra context to the existing contact page
response = super().contactus(**kw)
response.qcontext['custom_message'] = 'Welcome!'
return response
JSON Routes for AJAX
For AJAX calls from website pages, use type='json'. These routes accept JSON bodies and return JSON automatically:
@http.route('/books/availability', type='json',
auth='public', website=True)
def book_availability(self, book_id, **kw):
book = request.env['library.book'].browse(book_id)
return {'available': book.available_copies > 0}
- Add
website=Trueto@http.routeto activate website context andrequest.website - Use
slug(record)/unslug(slug_str)for SEO-friendly record URLs - Always check existence and publication status before rendering detail pages
- Allow website publishers to preview unpublished records via
request.website.is_publisher()
Frequently Asked Questions
What's the difference between auth='public' and auth='user' for website routes?
auth='public' allows access without login — anonymous visitors see the page. auth='user' requires a logged-in session (portal or internal user). Most public website pages use auth='public'. Portal pages use auth='user'.
How does Odoo generate slugs from record names?
slug(record) calls record.sudo().read(['name', 'id']), lowercases the name, replaces spaces and special characters with hyphens, and appends -{id}. The ID at the end ensures uniqueness even if two records share the same name.
Can I add URL query parameters to a website route?
Yes — declare them as keyword arguments in the controller method signature. Odoo maps URL parameters to kwargs automatically: def book_listing(self, page=1, search='', **kw) reads ?page=2&search=python from the URL.