先说结论
website_knowledge 不是简单把 Knowledge 文章“前台渲染一下”。
把 models/knowledge_article.py、controllers/main.py、models/website.py 和几组测试连起来看,Odoo 企业版真正实现的是一个可公开的知识子树机制,而不是“整库公开”机制。
它至少同时守住了 5 条边界:
- 公开不是单篇行为,而是树状传播行为:父文章一旦发布,子孙节点会一起继承
website_published; - 公开访问按可达祖先裁剪:访客能看到的,不是所有文章,而是某个公开根以下、且自己沿祖先链可达的节点;
- 侧边栏不是全量目录:只展示当前文章相关、且公开可访问的祖先 / 子节点;
- 前台搜索只搜已发布内容:
website.searchable.mixin接入后,Knowledge 被并入网站搜索,但 domain 明确锁在已发布文章; - 前台 SEO 不是后台字段直出:summary、OpenGraph、Twitter meta 都是根据文章 body 重新整理,嵌入块还会被排除。
所以更准确的理解是:
Odoo 企业版 Knowledge 的网站公开能力,本质上是在私有知识树里切出一段“可安全暴露给访客的公开子站”,而不是把后台知识库直接搬到前台。
这也是为什么它放在企业版 website_knowledge 里,而不是社区版随手加一个路由就算完成。
一、核心心智:它公开的是“子树”,不是“整库”
最容易误解的一点,是把 website_published 理解成普通网站页面上的“这篇文章是否公开”。
但 website_knowledge/models/knowledge_article.py 里的 write() 已经说明,官方模型不是按“单页”想的,而是按“树”想的。
当写入值里出现:
website_published- 或
is_published
模块会:
- 找到当前记录的所有
child_of后代; - 排除自己;
- 用
ignore_published_propagation上下文再次写入; - 把新的发布状态同步给整棵后代子树。
也就是说:
- 发布一个父节点,不只是发布父节点自己;
- 取消发布一个父节点,也不只是收回当前页;
- 它是在维护一段前台可访问的连续知识结构。
为什么官方要这样设计
因为知识库和博客不一样。
博客文章常常天然独立;但知识文章通常带有明显的层级:
- 产品说明
- 子主题
- FAQ
- 子流程
- 附属条目
如果只公开某个父节点,不公开其子节点,访客点开后目录会断裂; 如果只公开子节点,不公开路径中的父节点,导航和上下文又会失真。
所以官方的选择是:
一旦你决定把某个知识树根公开给网站,系统默认帮你把这一段结构保持完整。
这就是“发布传播”的真实产品含义。
二、为什么新建在已公开父节点下的文章会自动公开
_prepare_article_create_values() 里还有一个很关键的小动作。
当你在某个 parent_id 下创建新文章,而且当前操作者是内部用户或 su 时,如果父文章已经 website_published,新文章会直接带上:
website_published = True
这意味着官方不仅在“改状态”时做传播,连“往公开子树里继续长新节点”时也会自动继承公开性。
这条规则解决了什么问题
如果没有它,就会出现一种很烦的运营故障:
- 今天你公开了一个知识中心根节点;
- 明天同事在这个根下面补了一篇“退货 FAQ”;
- 结果后台看得到,网站前台却突然缺一块;
- 最后你还得再回去补勾一次网站发布。
Odoo 直接把这个坑堵死了。
测试 test_knowledge_published_propagation 也明确验证:
- 父节点发布后,孙辈也跟着公开;
- 在已公开节点下新建文章时,新的文章会自动公开;
- 但如果文章后来被移动成根节点,或被手动改成不公开,再移回公开树下,也不会无脑重新覆盖其状态。
这说明官方做得并不粗暴。它的策略不是“只要放进树里就强制公开”,而是:
- 初次承接公开上下文时,给出合理默认;
- 后续用户若明确改过状态,系统尊重人工决定。
这类边界非常企业化:
自动化负责减少漏操作,但不能永久剥夺人工控制。
三、公开访问为什么看的是“可访问根祖先”而不是单纯 parent 链
controllers/main.py 里最值得看的,不是路由本身,而是公开侧边栏和公开详情页如何判断“你现在到底属于哪棵公开树”。
比如:
/knowledge/article/<id>/knowledge/public/sidebar/knowledge/public/search
这些接口背后,都不只是简单按 parent_id 往上找。
特别是 get_public_sidebar_articles(),它先取:
_get_accessible_root_ancestors()
然后才基于这些 ancestor 生成前台可见目录。
这意味着什么
这意味着公开可见性不是“这篇文章自己勾了公开即可”,而是要看:
- 它是否属于一条对当前访客真正可达的祖先链;
- 这条祖先链上哪些节点是公开的;
- 当前节点在这个可达结构里应该展示哪些兄弟 / 子项。
测试 test_get_accessible_root_ancestor 很能说明这层设计:
- 若文章未发布,portal 用户拿不到任何可访问根祖先;
- 若某个中间 child 被发布,则 grandchild 会把 child 和自己都识别为可达祖先;
- 若改成通过成员邀请获得访问,则又会按成员权限重建可达祖先集合;
- 某个父节点权限被拿掉时,祖先集合会重新收缩。
所以 _get_accessible_root_ancestors() 的意义,不只是“找父节点”,而是在统一回答一个问题:
对于当前用户,这篇文章在什么结构里是可见且可导航的?
这就是为什么前台目录不会简单照搬后台树。
四、侧边栏为什么只显示“相关且可访问”的节点
_redirect_to_public_view() 里,模板变量 show_sidebar 的判断很克制:
- 若
no_sidebar=True,当然不显示; - 若当前文章没有任何已发布父级或已发布子级,也不显示。
而 get_public_sidebar_articles() 返回的数据同样经过裁剪:
- 必须
is_article_item = False; - 必须在可访问祖先集合里,或是这些祖先的直接子级;
- 并且会排除“当前文章自己作为 parent 的重复展开”场景。
测试 test_knowledge_public_article_routes 进一步把规则钉死了:
1. 公开根节点的侧边栏,只需要显示根本身
因为对访客而言,这是某个公开子站的入口页,而不是后台整库导航。
2. 当你打开公开子孙文章时,侧边栏会显示:
- 根节点;
- 中间父节点;
- 当前节点;
- 当前节点同层的公开兄弟(如果它们属于同一公开路径);
3. 未公开文章请求侧边栏时,直接返回空列表
这点很重要。官方不是“返回目录但点不开”,而是根本不把未公开结构暴露给前台。
这正是安全边界应该有的样子。
为什么这套目录裁剪很重要
因为知识库和普通 CMS 页面不同,知识库最大的价值之一就是“结构化导航”。
但如果导航不做裁剪,会立刻出现两个问题:
- 信息泄露:访客能看到不该看到的节点标题;
- 体验混乱:访客会看到一堆跳不过去的灰色目录。
Odoo 这里选择的是更稳的路线:
前台导航宁可少,也不展示不可访问结构。
五、为什么 article item 不进公开 children / sidebar
在测试里,官方专门构造了:
- 普通已发布子文章
- 未发布子文章
is_article_item=True的已发布 item
然后验证 /knowledge/public/children 只返回:
- 已发布、且不是 item 的节点
这背后其实体现了 Knowledge 的两种内容形态:
- 可作为知识树节点导航的 article;
- 更像叶子内容 / 内容项的 article item。
对于后台协作来说,两者都可能有意义; 但对网站前台公开导航来说,官方认为:
- 树形目录应该突出“结构节点”;
- item 不应该把导航层级搅乱。
这点特别值得做实施时记住。
很多团队会误把所有知识内容都做成同一层级的页面,最后网站知识中心变成:
- 层级太深,导航失控;
- 或叶子太多,目录无法浏览。
Odoo 这里其实已经给出暗示:
公开知识中心的导航,应该优先服务结构,而不是穷举所有内容碎片。
六、前台搜索为什么只搜已发布知识文章
models/website.py 里,website._search_get_details() 被扩展后,当搜索类型是:
allknowledge
会把 knowledge.article._search_get_detail() 挂进网站搜索。
而 _search_get_detail() 自己又明确加了 domain:
('is_published', '=', True)('is_template', '=', False)
并把:
namebodywebsite_url
纳入搜索与返回映射。
这说明两件事
1. Knowledge 前台搜索不是独立孤岛
只要接上 website.searchable.mixin,知识文章就会和站内其他公开内容一起进入网站搜索体系。
2. 但它的公开范围被限制得很明确
没发布的文章、模板文章,都不进前台搜索。
这就避免了一种很常见的事故:
- 正文页面没公开;
- 结果却能被站内搜索、搜索建议或 sitemap 间接暴露。
Odoo 在这里的取舍很清晰:
只要内容还没完成“可公开”这一步,就不让它进入任何前台发现链路。
这比很多 CMS 靠页面模板层面做“隐藏”要稳得多,因为它直接把限制写进搜索数据源层了。
七、为什么 summary 和 SEO 元信息要重新从 body 计算
很多人会忽略 get_website_meta() 和 _compute_summary(),但这部分恰恰最像“网站产品化”而不是“后台对象曝光”。
_compute_summary() 做了几件事:
- 如果 body 为空,summary 为空;
- 把 body 解析成 HTML 片段;
- 删除所有
data-embedded的元素; - 再把纯文本压平、去空白;
- 截前 100 个字符,超长补
...
get_website_meta() 再基于这个 summary 生成:
og:titleog:descriptiontwitter:titletwitter:descriptionmeta_description- 若存在 cover,还会补
og:image和twitter:image
为什么要先删掉 data-embedded
测试 test_knowledge_meta_tags 已经说得很明白:
- 目录块
- 文件块
- 嵌入视图
- 视频等特殊嵌入块
这些内容不应该污染前台摘要。
因为搜索引擎和社交卡片需要的是:
- 这篇文章在说什么;
- 而不是文章里嵌了哪些 UI 组件。
如果不做这一步,经常会出现很糟的 SEO 摘要:
- 一串目录标题;
- 一段按钮文案;
- 一些完全不适合被拿去当 description 的界面碎片。
所以官方这里不是简单把 body 截断,而是在做一次前台阅读语义重建。
这也是很多二开最容易偷懒、却最影响外部体验的地方。
八、公开路由为什么仍然保留“未发布就去登录”的分流
/knowledge/article/<id> 这条路由对 public user 的处理非常典型:
- 文章不存在:
NotFound - 文章存在且
website_published=True:转公开视图 - 文章存在但未发布:跳去
/web/login?redirect=...
这说明官方并没有把网站知识库简单设计成“两种状态:公开 / 404”。
它还保留了一条很重要的第三种可能:
- 这篇文章不是公开内容,但它可能是登录后有权看的内容。
这对企业知识中心非常重要。
因为很多团队并不是把 Knowledge 当纯官网内容,而是同时承接:
- 对外帮助中心
- 客户 Portal 文档
- 内部协作知识
- 混合可见性知识树
所以当 public user 访问到未公开文章时,系统不是武断地说“页面不存在”,而是说:
- “你作为公开访客不能看,但登录后也许可以。”
这条分流其实体现了 website_knowledge 的产品定位:
它不是独立 CMS,而是企业知识权限体系向网站前台延伸出来的一层公开视图。
九、对实施与二开的启发
1. 不要把 website_published 当普通单页开关
在 Knowledge 里,它更接近“公开子树开关”。
2. 公开知识中心最好按“根节点”规划,而不是逐篇零散发布
因为官方的导航、搜索和发布传播,都是围绕子树思维设计的。
3. 如果你要魔改前台目录,先确认不会泄露未公开节点标题
很多“增强导航”的需求,第一步就把安全边界弄丢了。
4. SEO 摘要别偷懒直接截 HTML
官方已经告诉你,嵌入块和正文摘要是两回事。
5. 内外混合知识库场景下,别强行把“未公开”做成 404
保留登录分流,往往更贴近真实业务:同一篇文档对匿名访客不可见,但对客户或成员可能可见。
一句话记忆法
Odoo 企业版
website_knowledge公开的不是一篇篇孤立文章,而是一段可访问、可导航、可搜索、且不会泄露私有结构的知识子树。
参考源码
enterprise/website_knowledge/models/knowledge_article.pyenterprise/website_knowledge/models/website.pyenterprise/website_knowledge/controllers/main.pyenterprise/website_knowledge/tests/test_knowledge_public.pyenterprise/website_knowledge/tests/test_knowledge_public_business.pyenterprise/website_knowledge/tests/test_knowledge_security.py
DISCUSSION
评论区