Odoo's website module includes basic SEO controls, but "basic" is accurate: per-page meta title and description editing, a generated sitemap, and an Open Graph image. Structured data (JSON-LD), canonical URLs, hreflang tags, and Core Web Vitals optimization all require custom work. Here is a realistic picture of what Odoo handles automatically versus what you build.
What Odoo Website includes by default#
Per-page SEO editor:
Each website page, blog post, product page, and event page has an SEO panel (visible in the website editor: Promote → Search Engine Optimization). Fields:
- Title (populates
and OG title) - Description (populates
and OG description) - Keywords (not used by modern search engines; can be ignored)
XML sitemap:
Automatically generated at /sitemap.xml. Includes:
- All published website pages.
- All published products (if eCommerce is enabled).
- All published blog posts.
- All events.
The sitemap updates when content publishes. Submit it to Google Search Console.
Robots.txt:
Generated at /robots.txt. Points to the sitemap. Configurable in Website → Configuration → Settings → SEO → Robots.txt.
Open Graph tags:
OG tags (og:title, og:description, og:image) are populated from the SEO editor fields. OG image requires the page to have a cover image set.
Adding JSON-LD structured data#
Odoo does not generate JSON-LD for products, blog posts, or events automatically. Add it via QWeb template inheritance.
Product schema (Product page):
<template id="product_page_jsonld" inherit_id="website_sale.product">
<xpath expr="//head" position="inside">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "<t t-esc="product.name"/>",
"description": "<t t-esc="product.description_sale or ''"/>",
"sku": "<t t-esc="product.default_code or ''"/>",
"offers": {
"@type": "Offer",
"priceCurrency": "<t t-esc="website.currency_id.name"/>",
"price": "<t t-esc="product.list_price"/>",
"availability": "https://schema.org/InStock"
}
}
</script>
</xpath>
</template>Blog post schema:
<template id="blog_post_jsonld" inherit_id="website_blog.blog_post_complete">
<xpath expr="//head" position="inside">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "<t t-esc="blog_post.name"/>",
"datePublished": "<t t-esc="blog_post.post_date"/>",
"author": {
"@type": "Person",
"name": "<t t-esc="blog_post.author_id.name"/>"
}
}
</script>
</xpath>
</template>Validate the output at Google's Rich Results Test.
Canonical URLs#
Odoo adds a tag to most pages pointing to the current URL. For product pages accessible via multiple URLs (e.g., category-filtered views), the canonical points to the product's base URL.
Override the canonical in edge cases:
<template id="custom_canonical" inherit_id="website.layout">
<xpath expr="//link[@rel='canonical']" position="replace">
<link rel="canonical" t-attf-href="https://#{website.domain}/products/#{product.id}"/>
</xpath>
</template>hreflang for multilingual sites#
If you run Odoo website in multiple languages, search engines need hreflang tags to understand which language version to serve in which region. Odoo does not generate these automatically.
Add them via a controller override:
# mymodule/controllers/main.py
from odoo.addons.website.controllers.main import Website
class WebsiteCustom(Website):
def _get_lang_alternates(self, **kwargs):
# Return list of {lang: 'en', url: '/en/page'} dicts
...And in the template:
<t t-foreach="lang_alternates" t-as="alt">
<link rel="alternate" t-att-hreflang="alt['lang']" t-att-href="alt['url']"/>
</t>Core Web Vitals and page speed#
Odoo website pages load Bootstrap, jQuery, owl.js, and the Odoo web client on every page. The default payload is heavy for a marketing site.
Practical optimizations:
- Disable unused modules. Each installed module may add assets to
web.assets_frontend. Uninstall modules not used on the website (HR, Project, etc.).
- Use a CDN. Website → Configuration → Settings → Website → CDN. Set a CDN URL for static assets. Odoo appends a version query string (
?t=12345) to bust caches.
- Image optimization. Odoo resizes uploaded images and serves them in WebP where supported. Use the image widget's "Quality" option to reduce file size. Prefer responsive images via
t-att-srcwith the Odoo image route (/web/image/product.template/1/image_1920).
- Lazy loading. Add
loading="lazy"to images below the fold. Odoo website editor does not add this automatically.
- Defer non-critical JS. Custom scripts added to
web.assets_frontendload synchronously. Move non-critical scripts to a deferred bundle or adddeferattribute via the asset template.
Testing#
After deploying SEO changes:
- Google Search Console → URL Inspection → verify meta tags.
- Google Rich Results Test → verify structured data.
- PageSpeed Insights (pagespeed.web.dev) → measure Core Web Vitals.
- Screaming Frog or Ahrefs → crawl the sitemap, check for broken canonical tags, duplicate meta descriptions.
The most impactful single change for most Odoo websites is image size reduction. Odoo's drag-and-drop editor makes it easy to upload 4 MB PNGs that appear as 300 px thumbnails.

