Advertisement
💻 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

Website Controllers: Mapping URLs to Python

A controller turns a URL into a Python method that returns a page. You subclass http.Controller and decorate methods with @http.route — this is how custom pages, form handlers, and JSON endpoints are built.

from odoo import http
from odoo.http import request

class MyWebsite(http.Controller):

    @http.route("/products", auth="public", website=True)
    def product_list(self, **kw):
        products = request.env["product.template"].sudo().search([])
        return request.render("mymod.product_list", {"products": products})

    @http.route("/products/<model('product.template'):prod>", auth="public", website=True)
    def product_detail(self, prod, **kw):
        return request.render("mymod.product_detail", {"product": prod})
auth=Who can access
publicanyone, even logged-out (runs as public user)
userlogged-in users only
noneno auth layer (special cases)

Key pieces: website=True wraps the page in the site's theme/menu; request.render(template, values) feeds data to a QWeb template; and the <model(...):name> converter turns a URL segment straight into a record. Security caution: on a public route you often need sudo() to read data (the public user has few rights) — but never sudo() then trust user input blindly, or you expose private records. Validate access on anything user-specific.

🏋️ Practical Exercise

  1. Create an HTTP controller with @http.route.
  2. Return a rendered template.
  3. Read a URL parameter.
  4. Set auth='public'.
  5. Pass data to the template.

🔥 Challenge Exercise

Write a website controller mapping a URL (e.g. /library/books) to a method that returns a rendered QWeb template with data from a model. Explain the @http.route decorator, the auth and website=True options, and how the controller passes context to the template.

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.

Advertisement

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

Interview Questions

  • What is a controller in Odoo?
  • What does @http.route do?
  • What does auth='public' mean?
  • How do you pass data to a website template?
  • What does website=True on a route do?

FAQ

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.