Ad – 728×90
💻 Website Development

Odoo Website Controllers – Building Public Web Pages

Website controllers are the Python layer that handles HTTP requests for public-facing pages. They differ from plain Odoo HTTP controllers in one key way: the website=True flag on @http.route, which activates website context, multi-website routing, and the theme layer.

⏱️ 22 min 🎯 Intermediate 📅 Updated 2026
What you'll learn:
  • How to write a website controller with website=True
  • Using request.website and request.env in 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:

Python
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 context
  • auth='public' — accessible without login
  • request.website — the active ir.website record

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:

Python
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:

Python
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:

Python
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.

Ad – 728×90

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:

Python
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:

Python
@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}
Key takeaways:
  • Add website=True to @http.route to activate website context and request.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.