先说结论
Odoo 的 sitemap.xml 不是“把所有 URL 导出来”这么简单。
website.models.website 在生成 sitemap 时,真正做的是一套 可抓取页面筛选机制:
- 页面记录要先满足公开与可访问条件;
- 控制器路由不是全部进入 sitemap,只有声明了
sitemap能力的才会参与; - 动态路由要靠 converter 去枚举真实记录,而不是把路由模板直接吐出去;
- 输出前还会做 URL 规范化和去重;
- 语言上下文默认回到网站默认语言,避免同一页面在多语言下无限膨胀。
所以它解决的核心问题不是“生成一个 XML 文件”,而是:
如何让搜索引擎看到值得抓、能够抓、抓了也不会脏掉索引的页面集合。
一、为什么 sitemap 不是路由清单,而是“公开内容清单”
很多人第一次接触 Odoo 网站,会误以为 sitemap 应该把系统里所有前台地址都输出给搜索引擎。
但源码恰恰说明,Odoo 不这么想。
在 website.py 的 sitemap 生成逻辑里,它先处理的是站点页面,再处理控制器路由。这个顺序很关键:
- 先把天然属于网站内容层的页面纳入考虑;
- 再看控制器里有没有明确支持 sitemap 的动态入口。
这说明 Odoo 把 sitemap 当成“可公开内容地图”,而不是技术层的 URL 列表。
对业务团队来说,这个差别很大:
- 内容页进入 sitemap,意味着你希望它被搜索引擎发现;
- 功能页、跳转页、参数页、实验页不一定应该进去;
- 能访问,不等于值得被索引。
如果把 sitemap 理解错,最常见的问题就是:
- 大量低价值页面被抓取;
- 搜索引擎预算浪费在筛选页、参数页或重复 URL 上;
- 真正重要的产品页、博客页、活动页反而抓不深。
二、为什么 Odoo 要区分“可枚举路由”和“不可枚举路由”
源码里最值得注意的一段,是它遍历 router.iter_rules() 之后,并不会无脑输出所有 rule。
它会先检查:
routing['sitemap']是否显式为False;sitemap是否是 callable;- rule 本身是否具备可枚举条件;
- 动态 converter 能不能真正生成记录。
这背后其实是在回答一个现实问题:
一个路由是不是能稳定列举出真实页面?
举几个典型例子:
/shop/product/<product>:可以,因为 product 记录能枚举;/blog/<blog>/<post>:可以,因为 blog/post 是真实对象;/search?q=...:通常不应该,因为它不是固定内容页;- 某些带鉴权、带上下文、带临时参数的控制器:也不该进 sitemap。
所以 Odoo 用 sitemap 钩子把“技术上存在的路由”和“内容上值得公开的页面”切开了。
这正是成熟网站系统的做法。
三、为什么动态路由不是吐模板,而是要靠 converter 逐条展开
源码里有一段很重要:它会读取 rule._converters,然后逐步构造 values,对每个 converter 调用 generate()。
这意味着 Odoo 生成 sitemap 时,并不会输出这种东西:
/shop/product/<product>/event/<event>/blog/<blog>/<post>
而是会尝试把它们展开为真实 URL:
/shop/product/odoo-bookcase-25/event/odoo-experience-2026/blog/product-team-1/seo-checklist-18
这一步特别重要,因为搜索引擎要的是“能抓的链接”,不是开发者理解的“路由模板”。
更细一点看,源码还会处理 converter 的 domain:
- 如果模型有
website_id字段,会自动补上当前网站的范围; - 如果查询字符串参与过滤,会转成 domain 再做筛选;
- 记录还会带上默认语言上下文。
这说明 sitemap 不是静态文件拼接,而是一次 基于 ORM 和当前网站上下文的内容枚举过程。
四、为什么 Odoo 要做 URL 去重和尾斜杠规范化
源码里专门定义了 _norm(url):
/保持不变;- 其他 URL 统一去掉尾部
/; - 再通过
url_set去重。
很多人会低估这一步的重要性。
如果不做规范化,很容易出现这些问题:
/page和/page/被当成两个地址;- 同一个 callable sitemap 被重复计算多次;
- 同一记录被不同 rule 展开后重复进入 sitemap;
- 搜索引擎看到重复 URL,canonical 信号被稀释。
Odoo 用非常务实的方法解决它:
- 先把 URL 归一;
- 再用集合去重;
- 对 callable sitemap 还做一次函数级去重,避免重复计算同一个 endpoint。
这说明它优化的不只是 SEO,还有生成成本。
对于记录多、模块多、路由多的网站,这种“少算一次、少吐一次”非常值钱。
五、为什么 sitemap 输出默认语言,而不是把所有语言版本一股脑塞进去
在源码里,无论是 callable sitemap 还是 converter 生成记录,Odoo 都反复带上:
with_context(lang=self.default_lang_id.code)
这透露出一个设计态度:
sitemap 的主输出,首先围绕网站默认语言建立。
这样做有几个好处:
- 避免每种语言都把同一套内容再列一遍,造成规模暴涨;
- 保证 slug、标题、查询结果在统一语言语境下生成;
- 让搜索引擎先抓到“主版本”,再通过其他机制处理多语言关系。
很多团队一看到多语言,就想把所有版本全部堆进 sitemap。
但如果内容翻译质量不一致、URL 规则不统一、公开语言还没全部准备好,这样反而会让索引质量下降。
Odoo 在默认实现里偏保守,实际上更适合大多数企业站。
六、为什么 lastmod 不是装饰字段,而是内容更新信号
源码在页面记录部分会汇总 last_dates,再写回 record['lastmod']。
这说明 Odoo 并不是只关心“页面存在”,还关心:
这个页面最近有没有值得重新抓取的变化。
虽然 lastmod 不保证搜索引擎一定按它行动,但它至少能传达出两层信号:
- 某些页面长期稳定;
- 某些页面最近刚更新,值得更快重抓。
对博客、活动、产品目录这种持续更新的网站来说,lastmod 的价值比很多人想象的大。
因为它影响的不是单个页面,而是整个站点的抓取节奏。
七、实战里最容易误解的 3 件事
1. 不是所有能访问的 URL 都应该进 sitemap
搜索页、实验页、临时活动页、带复杂参数的页面,经常“技术上能打开”,但并不适合作为索引入口。
2. 不是路由写了就会自动有好 sitemap
如果你的控制器没有正确声明 sitemap 逻辑,或者动态 converter 没法安全枚举,Odoo 不会帮你魔法补全。
3. sitemap 质量比数量更重要
一个塞满低质量 URL 的 sitemap,看起来很“全”,其实会拖累抓取预算。
八、给实施和运营团队的建议
如果你在做 Odoo 官网或内容站,应该把 sitemap 当成抓取策略的一部分来维护:
- 定期检查哪些页面真的应该公开索引;
- 对动态控制器明确设计 sitemap 行为;
- 避免把筛选页、重复页、参数页混进主 sitemap;
- 多语言站先明确默认语言与主索引版本;
- 对重要内容页关注更新时间是否能正确传达。
换句话说,sitemap 不是发布动作的尾巴,而是信息架构的一部分。
最后的结论
Odoo 的 sitemap 生成机制,本质上是在做三件事:
- 枚举真实公开内容;
- 过滤不适合抓取的技术路由;
- 把结果规范化后交给搜索引擎。
所以它从来都不是“自动导出 XML”那么简单。
真正值得学的地方在于:
Odoo 把 sitemap 当成“网站公开内容边界”的表达,而不是程序路由表的副本。
DISCUSSION
评论区