Your Odoo Website Is Invisible to Google by Default
A fresh Odoo 19 website installation ships with no meta descriptions, no structured data, auto-generated URL slugs full of IDs, uncompressed images, and zero integration with Google Search Console. From a search engine's perspective, your beautifully designed product pages might as well not exist.
We've audited Odoo websites that had been live for over a year with zero organic impressions — not because the content was bad, but because the technical SEO foundation was never configured. Odoo 19 ships with all the tools you need, but they require deliberate setup. The Website SEO module is not a "set it and forget it" checkbox; it's a toolkit that expects you to write meta titles, define URL patterns, inject JSON-LD, optimize images, and submit sitemaps.
This guide walks through every SEO lever available in Odoo 19's Website module — from the basics (meta tags and slugs) through advanced techniques (custom JSON-LD schemas, lazy-loaded WebP images, and Core Web Vitals tuning). Each section includes the exact steps, code, and configuration to go from invisible to indexed.
Clean URL Slugs: Removing IDs and Building SEO-Friendly Paths in Odoo 19
By default, Odoo generates URLs like /shop/product/ergonomic-desk-42 where 42 is the database ID. These IDs are ugly, leak internal information, and create duplicate content issues when the slug changes but the ID stays the same. Odoo 19 lets you fix this at multiple levels.
Step 1 — Edit Slugs in the Promote Tab
Open any page in the Website Builder, click Promote, and edit the URL field. Remove the numeric ID and use lowercase, hyphenated keywords. For example, change /shop/product/ergonomic-desk-42 to /shop/ergonomic-standing-desk. Odoo automatically creates a 301 redirect from the old URL to the new one.
Step 2 — Programmatic Slug Generation
For e-commerce stores with hundreds of products, manual slug editing doesn't scale. Use a server action or scheduled action to generate clean slugs from product names:
from odoo.addons.http_routing.models.ir_http import slugify
for product in env['product.template'].search([('website_published', '=', True)]):
# Generate a clean slug from the product name
clean_slug = slugify(product.name)
# Check for duplicates and append a suffix if needed
existing = env['product.template'].search([
('website_slug', '=', clean_slug),
('id', '!=', product.id),
])
if existing:
clean_slug = f"{{clean_slug}}-{{product.default_code or product.id}}"
product.website_slug = clean_slugStep 3 — URL Rewrite Rules for Category Pages
Product category URLs default to /shop/category/office-furniture-3. You can override these in Website → Configuration → URL Redirects or by editing the category's website_slug field directly. For a clean hierarchy like /shop/office-furniture/desks, set slugs at each category level and ensure the parent-child relationship is preserved.
Every time you change a slug in Odoo, it creates a 301 redirect. If you change a slug three times, Google follows a redirect chain: old-url-1 → old-url-2 → old-url-3 → current-url. Each hop slows crawling and leaks PageRank. After bulk slug cleanup, audit Website → Configuration → URL Redirects and delete intermediate redirects so every old URL points directly to the final destination.
Adding JSON-LD Structured Data to Odoo 19 for Rich Search Results
Structured data tells Google exactly what your page content represents — a product with a price and rating, an organization with a logo and address, a FAQ with questions and answers. Without it, Google guesses. With it, you qualify for rich results: star ratings in search, product prices, FAQ dropdowns, breadcrumb trails, and organization knowledge panels.
Step 1 — Organization Schema (Site-Wide)
Add an Organization schema to your website layout so it appears on every page. This powers the Google Knowledge Panel for your brand:
<template id="jsonld_organization" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "<t t-esc="website.name"/>",
"url": "<t t-esc="request.httprequest.host_url"/>",
"logo": "<t t-esc="request.httprequest.host_url"/>web/image/website/1/logo",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+1-555-000-0000",
"contactType": "sales"
},
"sameAs": [
"https://www.linkedin.com/company/your-company",
"https://twitter.com/your-company"
]
}
</script>
</xpath>
</template>Step 2 — Product Schema for E-Commerce Pages
Odoo 19's website_sale module includes basic product structured data, but it's missing key fields that Google requires for rich results: brand, sku, gtin, and aggregateRating. Extend it:
<template id="jsonld_product" 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"/>",
"image": "<t t-esc="request.website.image_url(product, 'image_1920')"/>",
"sku": "<t t-esc="product.default_code"/>",
"brand": {
"@type": "Brand",
"name": "<t t-esc="product.product_brand_id.name or website.name"/>"
},
"offers": {
"@type": "Offer",
"price": "<t t-esc="product.list_price"/>",
"priceCurrency": "<t t-esc="website.currency_id.name"/>",
"availability": "https://schema.org/InStock",
"url": "<t t-esc="request.httprequest.url"/>"
}
}
</script>
</xpath>
</template>Step 3 — FAQ Schema for Landing Pages
FAQ structured data creates expandable question-answer pairs directly in Google search results, effectively doubling your SERP real estate. Add it to any page with a FAQ section:
<template id="jsonld_faq_page" inherit_id="website.pages_template">
<xpath expr="//head" position="inside">
<t t-if="main_object and hasattr(main_object, 'faq_ids')">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
<t t-foreach="main_object.faq_ids" t-as="faq">
{
"@type": "Question",
"name": "<t t-esc="faq.question"/>",
"acceptedAnswer": {
"@type": "Answer",
"text": "<t t-esc="faq.answer"/>"
}
}<t t-if="not faq_last">,</t>
</t>
]
}
</script>
</t>
</xpath>
</template>Step 4 — BreadcrumbList Schema
Breadcrumb structured data shows your site hierarchy in search results (Home > Shop > Category > Product) instead of the raw URL. Odoo 19 does not generate this by default:
<template id="jsonld_breadcrumb" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "<t t-esc="request.httprequest.host_url"/>"
},
{
"@type": "ListItem",
"position": 2,
"name": "<t t-esc="main_object._description or 'Page'"/>",
"item": "<t t-esc="request.httprequest.url"/>"
}
]
}
</script>
</xpath>
</template>Always test your structured data with Google's Rich Results Test (search.google.com/test/rich-results) before pushing to production. Invalid JSON-LD won't crash your site — it just silently fails, and you won't know until you check Search Console weeks later and see zero rich results.
Image Optimization in Odoo 19: WebP, Lazy Loading, and Alt Text at Scale
Images are typically 60-80% of a page's total weight. Odoo 19 stores product images at their original upload resolution (often 4000x4000 from a photographer) and resizes on-the-fly via the /web/image controller. Without optimization, a product listing page with 20 items can exceed 15MB of image data.
Step 1 — Enable WebP Conversion
Odoo 19 supports automatic WebP conversion when the browser sends an Accept: image/webp header. Enable it in Website → Configuration → Settings → Images. This typically reduces image sizes by 25-35% compared to JPEG at the same quality level.
Step 2 — Set Maximum Upload Dimensions
In the same settings panel, set Max Image Resolution to 1920x1920. This automatically downscales any uploaded image that exceeds those dimensions. A 4000x4000 product photo at maximum quality is 8-12MB; at 1920x1920 it's 200-400KB — and visually indistinguishable on any screen.
Step 3 — Lazy Loading for Below-the-Fold Images
Odoo 19 adds loading="lazy" to images rendered through the Website Builder by default. But images injected via custom QWeb templates or HTML snippets may not have it. Ensure all image tags include the attribute:
<!-- Correct: lazy loading with explicit dimensions -->
<img t-att-src="request.website.image_url(product, 'image_512')"
t-att-alt="product.name"
loading="lazy"
width="512"
height="512"
class="product-thumb"
/>
<!-- Wrong: no lazy loading, no dimensions, no alt text -->
<img t-att-src="request.website.image_url(product, 'image_1920')"/>Step 4 — Bulk Alt Text via Server Action
Missing alt text is one of the most common SEO audit findings on Odoo sites. Product images inherit the product name as alt text automatically, but banner images, category images, and custom snippet images often have empty alt attributes. Use a scheduled action to audit and fix:
import re
from lxml import html
pages = env['website.page'].search([('website_published', '=', True)])
missing_alt = []
for page in pages:
if not page.arch:
continue
tree = html.fromstring(page.arch)
images = tree.xpath('//img[not(@alt) or @alt=""]')
if images:
missing_alt.append({{
'page': page.name,
'url': page.url,
'count': len(images),
}})
# Log results — fix manually or generate alt text from context
for entry in missing_alt:
_logger.warning(
"SEO: Page '%s' (%s) has %d images without alt text",
entry['page'], entry['url'], entry['count']
) Odoo stores images in multiple resolutions: image_1920, image_1024, image_512, image_256, and image_128. For product listing grids, image_512 is more than sufficient. Using image_1920 in a thumbnail grid wastes 10x the bandwidth and destroys your Largest Contentful Paint score.
Odoo 19 Page Speed Optimization: Core Web Vitals and Asset Bundling
Google uses Core Web Vitals — Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) — as ranking signals. A default Odoo 19 installation typically scores 30-45 on Google PageSpeed Insights for mobile. With the optimizations below, we consistently achieve 75-90.
Step 1 — Enable Asset Bundling and Minification
In production mode, Odoo 19 automatically bundles and minifies CSS and JavaScript. Verify this is active by checking that debug mode is disabled and the --dev flag is not set in your Odoo configuration:
[options]
; Ensure these are set for production
server_wide_modules = base,web
; Do NOT set dev_mode in production — it disables asset bundling
; dev_mode =
; Do NOT enable debug assets — it serves unbundled JS/CSS
; debug_mode = assetsStep 2 — Preload Critical CSS and Fonts
Odoo loads all website CSS in a single bundle, which can exceed 500KB. While you can't split the bundle without modifying core, you can preload critical resources to improve perceived load time:
<template id="preload_critical" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<!-- Preload main font to avoid FOIT (Flash of Invisible Text) -->
<link rel="preload"
href="/web/static/lib/fontawesome/fonts/fontawesome-webfont.woff2"
as="font" type="font/woff2" crossorigin="anonymous"
/>
<!-- Preconnect to external services -->
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://www.google-analytics.com"/>
</xpath>
</template>Step 3 — Defer Non-Critical JavaScript
Odoo 19 loads its JavaScript bundle synchronously in the <head>, blocking rendering. You can move non-critical scripts to the bottom of the body or add defer attributes via a custom module:
from odoo import http
from odoo.addons.website.controllers.main import Website
class WebsitePerf(Website):
def _get_asset_bundle(self, bundle_name, **kw):
"""Add defer to non-critical JS bundles."""
bundle = super()._get_asset_bundle(bundle_name, **kw)
# Only defer frontend bundles, not backend
if 'website' in bundle_name and 'backend' not in bundle_name:
bundle.js_defer = True
return bundleStep 4 — Reduce Cumulative Layout Shift (CLS)
CLS happens when elements shift position after the page starts rendering — usually caused by images without explicit dimensions, late-loading fonts, or dynamically injected banners. The fixes:
| CLS Cause | Odoo Context | Fix |
|---|---|---|
| Images without dimensions | Product grids, banners | Always set width and height on <img> tags |
| Font swap flash | Custom Google Fonts | Use font-display: swap and preload the WOFF2 file |
| Cookie consent banner | Odoo's built-in cookie bar | Set a fixed height with CSS: .o_cookies_bar {{ min-height: 80px; }} |
| Lazy-loaded above-the-fold images | Hero banners | Remove loading="lazy" from the first visible image |
Sitemap.xml and Robots.txt Configuration in Odoo 19
Odoo 19 auto-generates a sitemap at /sitemap.xml and a robots.txt at /robots.txt. Both work out of the box but need tuning for production sites with large catalogs or multi-website setups.
Step 1 — Verify Sitemap Generation
Visit https://yoursite.com/sitemap.xml. Odoo generates a sitemap index that links to individual sitemaps per model (pages, products, blog posts). Each sitemap contains up to 45,000 URLs. Verify that:
- All published pages appear in the sitemap
- Unpublished or archived pages are excluded
- URLs use your canonical domain (not
localhostor an IP address) - The
<lastmod>dates reflect actual content changes
Step 2 — Exclude Low-Value Pages from the Sitemap
Pages like /shop/cart, /my/account, and filter URLs (/shop?order=price) should not be in the sitemap. Odoo excludes most of these by default, but custom controllers may leak through. Control indexing per page:
from odoo import http
class CustomController(http.Controller):
@http.route('/custom/report', type='http', auth='public', website=True,
sitemap=False) # <-- This excludes the route from sitemap.xml
def custom_report(self, **kw):
return request.render('module.report_template')Step 3 — Customize Robots.txt
Odoo 19 generates a default robots.txt. Customize it via Website → Configuration → Settings → SEO or by overriding the controller. A production-ready robots.txt for an Odoo e-commerce site:
User-agent: *
Allow: /
# Block backend and utility paths
Disallow: /web/
Disallow: /web/database/
Disallow: /my/
Disallow: /shop/cart
Disallow: /shop/checkout
Disallow: /shop/payment
Disallow: /shop/confirmation
Disallow: /website/info
# Block faceted navigation (duplicate content)
Disallow: /shop?order=
Disallow: /shop?ppg=
Disallow: /shop?search=
# Block assets and API endpoints
Disallow: /xmlrpc/
Disallow: /jsonrpc
# Sitemap location
Sitemap: https://yoursite.com/sitemap.xml A Disallow in robots.txt tells crawlers not to crawl the URL, but it doesn't prevent indexing. If another site links to a disallowed page, Google may still index the URL (showing a snippet like "No information is available for this page"). To truly prevent indexing, use a <meta name="robots" content="noindex"> tag instead — and make sure the page is still crawlable so Google can see the noindex directive.
Connecting Odoo 19 to Google Search Console for Indexing and Performance Data
Google Search Console (GSC) is the only way to know how Google sees your site — which pages are indexed, which have errors, and which queries drive impressions. Odoo 19 supports all four GSC verification methods.
Step 1 — Verify Site Ownership
The fastest method is HTML tag verification. In GSC, choose "HTML tag" and copy the meta tag. In Odoo, go to Website → Configuration → Settings → SEO and paste the verification code into the Google Search Console field. Alternatively, add it to a QWeb template:
<template id="gsc_verification" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<meta name="google-site-verification"
content="YOUR_VERIFICATION_CODE_HERE"
/>
</xpath>
</template>Step 2 — Submit Your Sitemap
In GSC, navigate to Sitemaps and submit https://yoursite.com/sitemap.xml. Google will crawl the sitemap index and discover all sub-sitemaps. Check back after 24-48 hours to verify the sitemap status shows "Success" with the correct URL count.
Step 3 — Request Indexing for Key Pages
After launching a new product or publishing a blog post, use GSC's URL Inspection tool to request immediate indexing. Paste the URL, wait for the inspection, and click "Request Indexing." This is significantly faster than waiting for Google's natural crawl cycle, which can take days to weeks.
Step 4 — Monitor Core Web Vitals in GSC
The Core Web Vitals report in GSC shows real-user performance data grouped into "Good," "Needs Improvement," and "Poor" buckets. Common issues on Odoo sites:
| GSC Issue | Root Cause in Odoo | Fix |
|---|---|---|
| LCP > 2.5s | Hero banner using image_1920 without lazy loading disabled | Use image_1024, add fetchpriority="high", remove loading="lazy" |
| CLS > 0.1 | Product images without width/height attributes | Set explicit dimensions on all <img> tags |
| INP > 200ms | Heavy JavaScript bundles blocking main thread | Defer non-critical JS, remove unused website snippets |
5 Odoo 19 SEO Mistakes That Silently Destroy Your Search Rankings
Duplicate Content from Multi-Language Without Hreflang Tags
Odoo 19's multi-language feature creates separate URLs for each language (/en/shop, /fr/shop). Without hreflang tags, Google treats these as duplicate content and picks one version to index — often the wrong one. Odoo 19 generates hreflang tags automatically only if all languages are published. If you have a language installed but not published on the website, the hreflang tag set is incomplete, causing Google to ignore all of them.
Either publish all installed languages or uninstall unused ones. Verify hreflang output by viewing page source and searching for rel="alternate" hreflang=. Every language variant must reference all other variants, including itself.
Canonical URLs Pointing to HTTP Instead of HTTPS
If proxy_mode = True is not set in odoo.conf when running behind a reverse proxy, Odoo generates canonical URLs with http:// instead of https://. Google sees the canonical as a different URL from the one it crawled, leading to indexing confusion. This single misconfiguration can cause 50% of your pages to be flagged as "Duplicate, Google chose different canonical" in Search Console.
Set proxy_mode = True in odoo.conf and ensure your reverse proxy sends X-Forwarded-Proto: https. Verify by viewing the page source and confirming the <link rel="canonical"> tag uses https://.
Sitemap Includes Unpublished Test Pages
During development, teams create test pages, draft blog posts, and placeholder products. If these are marked as "published" (even briefly), Odoo adds them to the sitemap. Unpublishing later removes them from the website but Google may have already crawled and indexed them. The cached sitemap entry persists until the next sitemap regeneration.
Never publish test content on a production domain. Use a staging environment. If test pages were published, unpublish them, clear the sitemap cache by visiting /sitemap.xml?__cache=0, and use GSC's URL Removal tool to de-index them immediately.
Missing Trailing Slash Consistency Causes Duplicate Indexing
Odoo serves the same content at /shop and /shop/ without a canonical preference or redirect. Google may index both, splitting PageRank between two identical pages. This compounds across your entire site — every page effectively has two URLs competing with each other.
Add an Nginx rewrite rule to enforce one pattern (we prefer no trailing slash): rewrite ^(.+)/$ $1 permanent;. This issues a 301 redirect from the trailing-slash version to the clean version, consolidating all link equity on a single URL.
JSON-LD Errors from Dynamic Content with Special Characters
Product names and descriptions containing quotes, ampersands, or HTML entities break JSON-LD when injected directly via t-esc. A product named 18" Monitor & Stand generates invalid JSON because the unescaped quote terminates the string. Google silently ignores the entire structured data block for that page.
Use json.dumps() to properly escape dynamic values in JSON-LD. In QWeb, pass data through a Python controller that serializes the JSON-LD object, rather than building JSON strings inline in the template.
What Proper SEO Configuration Returns for Your Odoo Investment
Every Odoo website has a hosting cost. SEO determines whether that hosting cost generates returns or sits idle. Here's what we've measured across client implementations:
Clients who implement meta tags, structured data, and sitemap submission see 3-5x organic traffic within 6 months compared to default Odoo SEO settings.
Rich results from structured data (star ratings, prices, FAQ) increase CTR by 40% compared to plain blue links — same ranking position, more clicks.
Image optimization, asset bundling, and lazy loading double the PageSpeed score from 30-45 to 75-90 — crossing Google's "Good" threshold for Core Web Vitals.
The cost of not doing SEO is invisible: you never see the customers who searched for your product, found your competitor instead, and bought there. Paid ads stop generating traffic the moment you stop paying. Organic search compounds — every page you optimize continues generating traffic indefinitely.
Optimization Metadata
Complete Odoo 19 SEO guide. Configure meta tags, JSON-LD structured data, URL slugs, image optimization, page speed, sitemap.xml, robots.txt, and Google Search Console.
1. "Configuring Meta Titles and Descriptions in Odoo 19 for Every Page Type"
2. "Adding JSON-LD Structured Data to Odoo 19 for Rich Search Results"
3. "5 Odoo 19 SEO Mistakes That Silently Destroy Your Search Rankings"