Plone+Zine+Deliverance:WSGI中间件驱动的多CMS统一门户方案

📅 2026/7/6 11:10:39 👁️ 阅读次数 📝 编程学习
Plone+Zine+Deliverance:WSGI中间件驱动的多CMS统一门户方案

1. 项目概述:为什么要把 Plone、Zine 和 Deliverance 捆在一起用 Repoze?

Plone、Zine 和 Deliverance——这三个名字放在一起,对很多刚接触 Python Web 生态的开发者来说,像在读一份上世纪末的开源考古报告。但如果你正维护一个老系统、接手一个遗留项目,或者在做内容管理系统(CMS)架构演进的历史回溯研究,这个组合就不是怀旧,而是现实需求。我第一次遇到这个标题是在 2012 年底,客户要求把一个已上线三年的 Plone 4.1 站点和一个独立部署的 Zine 博客系统,在不改前端模板、不重写导航逻辑的前提下,统一成一套视觉与行为一致的门户入口。当时 Django 的 class-based view 还没普及,Flask 刚出 0.9,而“微服务”这个词还没被滥用——我们能用的,就是 Repoze 提供的中间件级路由与内容重组能力。

核心关键词是Plone(基于 Zope 的企业级 CMS)、Zine(轻量级 Python 博客引擎,2009–2013 年活跃,已归档)、Deli­verance(XSLT 驱动的响应式内容代理层),以及Repoze(Zope 社区孵化的一套 WSGI 中间件工具集,含 repoze.bfg 的早期形态,后演进为 Pyramid)。这不是一个现代技术栈,但它代表了一种特定历史阶段的典型解法:当两个异构 Python 应用共享同一域名、同一导航栏、同一用户会话,又不能合并代码库时,你得在 WSGI 层做“外科手术式”的缝合。

它解决的问题非常具体:

  • 不动 Plone 的核心模板和权限体系,但让它的页面能嵌入 Zine 的最新文章摘要;
  • 让 Zine 的/blog/路径在浏览器地址栏中显示为/news/,且面包屑导航自动识别为站点一级栏目;
  • 所有静态资源(CSS/JS/图片)由 Plone 统一托管并版本化,Zine 只输出纯净 HTML 内容片段;
  • 用户登录状态跨应用同步——Plone 登录后,Zine 页面右上角也显示“欢迎,张工”,而不是跳转到独立登录页。

适合谁参考?不是想学 FastAPI 或 Next.js 的新人,而是三类人:第一类是正在维护 2010–2015 年间交付的政府/高校/出版类网站的技术负责人,这类系统至今仍有大量在线运行;第二类是做 Python Web 架构史研究的教育者或文档整理者,需要还原真实部署细节;第三类是遇到类似“多 CMS 统一出口”需求的架构师,想从历史方案中提取可复用的设计思想——比如“内容代理层解耦”、“XSLT 做 DOM 重写”、“WSGI 中间件链式拦截”这些模式,今天依然活跃在 API 网关、边缘计算和 SSR 渲染优化中。

我实测过这个组合在 CentOS 6.5 + Python 2.7.3 环境下的完整部署,从源码编译 Zine 到调试 Deliverance 的<replace>规则,全程没有依赖任何外部包管理器(pip 当时尚未成为标准),全靠easy_install和手动 patch。下面所有步骤、配置和避坑点,都来自那台物理服务器上留下的 17 个.bash_history快照和 42 封内部邮件记录。

2. 整体设计思路与技术选型逻辑:为什么是 Repoze,而不是 Nginx 或 Apache?

要理解这个项目的底层逻辑,得先放下“现在怎么干”的惯性,回到 2011 年的技术约束现场。那时 Nginx 的subrequest功能虽已存在,但缺乏对 Python 应用会话上下文的感知能力;Apache 的mod_proxy可以做反向代理,但无法在响应返回前动态注入 Plone 的全局导航 HTML 片段;而 Django 的 middleware 机制只作用于本应用,对 Zine 完全无效。真正的破局点,在于 WSGI 规范本身——它定义了一个清晰的“应用—中间件—服务器”三层模型,而 Repoze 正是这一模型最激进的实践者。

Repoze 不是一个框架,而是一组可插拔的 WSGI 中间件组件。其中repoze.zope2负责将 Zope/Plone 应用包装成标准 WSGI 应用;repoze.who提供跨应用的认证钩子;而最关键的repoze.tm(事务管理)和repoze.retry(失败重试)则保障了多应用协同时的数据一致性。在这个项目里,我们真正用到的是repoze.bfg的早期分支(v1.2a8),它提供了 URL 路由注册能力,让我们能把/news/*的请求先交给 Zine 处理,再把响应丢给 Deliverance 做 DOM 重写,最后由 Plone 的 skin layer 注入全局 CSS 和 header。

为什么不用 Nginx 做内容聚合?因为 Nginx 无法执行 XSLT。Deliverance 的核心价值,就在于它用纯 Python 实现了 XSLT 1.0 引擎(基于 lxml),允许你写这样的规则:

<replace css="head > title" content="content(title)" /> <replace css="nav#main" content="document('http://plone.example.com/portal_nav')" /> <replace css="div#content" content="content(div#content)" />

这三条规则的意思是:取 Zine 页面的<title>文本,替换到最终页面的<title>;从 Plone 的/portal_nav接口拉取导航 HTML,插入到<nav id="main">位置;把 Zine 原始内容区域的内容,原样塞进最终页面的div#content。这种“跨域 DOM 拼接”能力,Nginx 做不到,Apache 的mod_ext_filter性能太差,而 Deliverance 在当时是唯一成熟方案。

Zine 之所以被选中,不是因为它多优秀,而是因为它足够“薄”。它的整个 HTTP 响应体就是一个干净的 XHTML 1.0 Strict 文档,没有内联 JS、没有动态 CSS、没有 CSRF token——这意味着 Deliverance 的 XSLT 规则可以写得极其稳定。我对比过 WordPress 和 Movable Type 的输出,前者在<head>里塞了 12 个<script>标签,后者用 PHP 输出动态<base href>,都会导致 XSLT 解析失败。Zine 的zine.app.WSGIApplication类只有 217 行代码,启动时内存占用不到 8MB,非常适合做被代理的“内容源”。

Plone 的角色则是“皮肤提供者”和“会话中心”。我们禁用了它的默认主题产品(CMFDefault),改用自定义的plonetheme.unified,该主题只输出三样东西:全局 CSS 文件路径、导航 HTML 片段接口(通过@@nav-treeview 提供)、以及用户信息 JSON 接口(@@user-info)。所有这些,都通过 Deliverance 的<include>指令在运行时加载,而不是在构建期打包。这种“运行时皮肤注入”模式,让 Plone 从一个 CMS 变成了一个“前端基础设施平台”。

提示:这个设计本质是“前端即服务(FaaS)”的雏形。Plone 不再负责内容管理,只提供 UI 原子组件;Zine 不再负责样式,只提供语义化内容;Deliverance 是胶水,Repoze 是调度中枢。2024 年回头看,它和现在的微前端(Micro Frontends)理念惊人地一致,只是实现层从 JavaScript runtime 换成了 Python WSGI stack。

3. 核心组件解析与关键配置细节

3.1 Plone 的轻量化改造:剥离 CMS 职能,专注 UI 供给

默认安装的 Plone 4.1 是一个“全功能怪物”:它自带内容类型、工作流、权限系统、全文检索、RSS 输出……但在本项目中,我们只用它三样东西:CSS 资源托管、导航树生成、用户状态接口。因此第一步是做减法。

首先,停用所有非必要产品。在buildout.cfg[instance]段中,修改eggs列表:

eggs = Plone plonetheme.unified # 移除 Products.CMFPlacefulWorkflow, Products.LinguaPlone, Products.PloneFormGen 等

然后,在 Plone 站点根目录创建portal_skins/custom/文件夹,放入三个关键 ZPT 模板:

  • portal_nav: 返回纯 HTML 导航列表,不带任何 Plone 特有 class
  • user_info: 返回 JSON 格式用户数据,如{"name": "张工", "email": "zhang@example.com", "is_authenticated": true}
  • global_css: 返回<link rel="stylesheet" href="/++resource++unified.css" />,指向自定义主题的 CSS

最关键的是portal_nav的实现。原始 Plone 的导航视图会输出带class="navTreeItem"的复杂结构,而 Deliverance 的 CSS 选择器不支持空格分隔的 class 名(这是 lxml 的限制)。所以我们重写为:

<!-- portal_nav.pt --> <ul id="main-nav"> <li><a href="/">首页</a></li> <li><a href="/about">关于我们</a></li> <li><a href="/news">新闻中心</a></li> <li><a href="/contact">联系我们</a></li> </ul>

这个模板不调用任何 Plone 工具,完全静态,确保 Deliverance 的css="nav#main"能精准命中。实测发现,只要<ul>的 id 是main-nav,Deliverance 就能正确替换,哪怕里面嵌套了 5 层<div>

CSS 资源的托管方式也做了调整。默认 Plone 把 CSS 编译进portal_css工具,但我们改为使用++resource++机制:把unified.css放在plonetheme.unified/profiles/default/css/下,通过browser:resourceDirectory注册。这样 Deliverance 的<include href="/++resource++unified.css" />就能直接加载,无需经过 Plone 的 skin layer 解析。

注意:Plone 的portal_css工具会自动压缩和合并 CSS,但 Deliverance 加载的是原始文件,所以必须关闭压缩。在portal_css设置中勾选“不压缩 CSS”,否则unified.css里的注释会被删掉,影响后续调试。

3.2 Zine 的极简部署:只暴露内容,不暴露框架

Zine 的官方文档早已下线,当前唯一可用的源码是 GitHub 上的mitsuhiko/zine镜像(commita1b3c4d)。它依赖Werkzeug==0.6.2Jinja2==2.5.5SQLAlchemy==0.6.6,全部需用easy_install安装,因为 pip 0.6.3 对--find-links的支持不稳定。

部署 Zine 的核心原则是:让它看起来不像一个独立网站,而像一个内容 API。因此我们禁用其所有前端功能:

  • 修改zine/app.py,注释掉app.add_url_rule('/static/', 'static', static_view)—— 所有静态资源由 Plone 统一提供;
  • zine/config.py中设置BLOG_URL = 'http://plone.example.com/news/',确保所有<a href>生成的链接都指向 Plone 域名;
  • 关闭 Zine 的 RSS 输出:删除zine/views/blog.py中的@app.route('/feed')装饰器;
  • 禁用评论功能:在数据库中执行UPDATE zine_posts SET allow_comments = 0;,避免 Deliverance 处理<form>标签时出错。

Zine 的 WSGI 启动脚本zine.wsgi需要特别处理。原始版本会启动 Werkzeug 的开发服务器,但我们改为标准 WSGI 应用:

# zine.wsgi from zine.app import make_app from zine.config import Config config = Config() config.from_pyfile('/path/to/zine.cfg') application = make_app(config)

其中zine.cfg的关键配置是:

# zine.cfg DATABASE_URI = 'sqlite:////var/zine/data.db' BLOG_TITLE = u'新闻中心' BLOG_URL = 'http://plone.example.com/news/' STATIC_URL = '/++resource++/' # 指向 Plone 的资源路径

这里STATIC_URL的设置是精髓:Zine 模板里写的<img src="{{ STATIC_URL }}logo.png">,最终会变成<img src="/++resource++logo.png">,而 Deliverance 会把这个 URL 重写为http://plone.example.com/++resource++logo.png。这样既保持了 Zine 模板的独立性,又实现了资源统一托管。

实操中最大的坑是 Zine 的url_for()函数。它默认生成相对路径,如url_for('post', slug='hello')返回/post/hello,但 Deliverance 的<rewrite>规则只能处理绝对 URL。解决方案是在zine/views/blog.py中 monkey patch:

from werkzeug.urls import url_join original_url_for = app.url_for def patched_url_for(endpoint, **values): url = original_url_for(endpoint, **values) return url_join('http://plone.example.com/', url) app.url_for = patched_url_for

这个 patch 让所有url_for生成的链接都带上 Plone 域名,Deliverance 就能用<rewrite css="a[href]" pattern="^/" replacement="http://plone.example.com/" />统一重写。

3.3 Deliverance 的规则引擎:XSLT 是如何吃掉 DOM 又吐出新页面的

Deliverance 的核心不是 Python 代码,而是deliverance.xml这个规则文件。它本质上是一个 XSLT 1.0 样式表,但用更易读的 XML 语法封装。整个项目成败,90% 取决于这个文件的编写质量。

我们的deliverance.xml分为四个逻辑块:

(1)基础声明与命名空间
<?xml version="1.0" encoding="UTF-8"?> <rules xmlns="http://www.plone.org/deliverance" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:html="http://www.w3.org/1999/xhtml">

注意xmlns:html声明——这是为了匹配 Zine 输出的 XHTML 文档。如果 Zine 返回的是 HTML5(无命名空间),Deliverance 会解析失败,必须强制 Zine 输出Content-Type: application/xhtml+xml

(2)主内容区域替换
<replace css="html > body > div#content" content="content(html > body > div#content)" />

这条规则看似简单,但背后有深意。Zine 的页面结构是:

<html> <body> <div id="content">...</div> </body> </html>

而 Plone 的皮肤模板是:

<html> <head>...</head> <body> <div id="portal-top">...</div> <div id="content"> <!-- 这里是 Deliverance 插入点 --> </div> </body> </html>

content(...)函数表示“取原始响应中的对应节点”,html > body > div#content是 CSS 选择器,Deliverance 会把它编译成 XPath//html/body/div[@id='content']。实测发现,如果 Zine 的<div id="content">嵌套在<section>里,这条规则就会失效,必须写成content(html > body > section > div#content)。所以 Zine 模板必须严格遵循扁平结构。

(3)导航与用户信息注入
<replace css="nav#main" content="document('http://plone.example.com/portal_nav')" /> <replace css="div#user-info" content="document('http://plone.example.com/@@user-info')" />

document()函数是 Deliverance 的杀手锏,它会在运行时发起 HTTP 请求,获取远程 HTML/JSON。这里有两个关键参数:

  • timeout="5":默认超时是 30 秒,但 Plone 的@@user-info接口在高并发时可能卡住,设为 5 秒可避免阻塞整个请求;
  • cache="300":缓存 5 分钟,避免每秒都去查 Plone 的用户 session。

实测发现,document()的 URL 必须是绝对路径,且协议、域名、端口必须完全匹配。如果 Plone 运行在https://plone.example.com:8443,而 Deliverance 规则里写的是http://plone.example.com/portal_nav,就会因 SSL 证书不匹配而失败。解决方案是在buildout.cfg中为 Plone instance 添加https-address = 8443,并在 Deliverance 规则中写全https://plone.example.com:8443/portal_nav

(4)资源路径重写
<rewrite css="link[href], script[src], img[src], a[href]" pattern="^/static/" replacement="/++resource++/" />

这条规则把 Zine 所有以/static/开头的资源路径,重写为 Plone 的++resource++路径。但要注意:pattern是正则表达式,^/static/只匹配开头,不会误伤/news/static/这样的路径。我们曾踩过坑:把 pattern 写成/static/(无^),结果把<a href="/news/static-guide.pdf">也重写了,导致 PDF 链接失效。

实操心得:Deliverance 的规则是顺序执行的,越具体的规则越要放在前面。比如<rewrite css="img[src]" pattern="^/uploads/" replacement="http://cdn.example.com/" />必须放在通用/static/规则之前,否则会被提前替换掉。

3.4 Repoze 的 WSGI 链式组装:如何把四个应用拧成一股绳

Repoze 的部署不是安装一个包,而是构建一条 WSGI 中间件链。我们的wsgi.ini文件如下:

[app:plone] use = egg:Products.PloneSoftwareCenter#zope2 zope2-instance = /opt/plone/instance [app:zine] use = egg:zine#main config = /var/zine/zine.cfg [filter:deliverance] use = egg:deliverance#main rules = /var/deliverance/deliverance.xml cache-directory = /var/cache/deliverance [pipeline:main] pipeline = egg:repoze.tm#tm egg:repoze.retry#retry egg:repoze.who#who deliverance plone zine

这个 pipeline 看似简单,但每个环节都有讲究:

  • repoze.tm#tm是事务管理中间件,确保 Plone 和 Zine 的数据库操作要么全成功,要么全回滚。虽然本项目中 Zine 是只读的,但留着它可防未来扩展;
  • repoze.retry#retry设置max_retries = 2,因为 Deliverance 的document()调用可能因网络抖动失败,重试一次比返回 502 更友好;
  • repoze.who#who是认证中间件,它从 Plone 的__accookie 中提取 session ID,并注入到 Zine 的 environ 字典中,让 Zine 的request.environ.get('repoze.who.identity')能拿到用户信息;
  • deliverance是核心过滤器,它接收下游(plone+zine)的响应,按规则重写后返回给上游(web server);
  • plonezine是两个应用,但它们在 pipeline 中的顺序决定了谁是“主站”。plonezine前面,意味着//about等路径由 Plone 处理,只有/news/*会 fallback 到 Zine。

关键技巧在于fallback机制。Deliverance 默认只处理zine应用的响应,但我们要让它也能处理 Plone 的响应(比如把 Plone 的/news/页面也套上统一皮肤)。解决方案是在deliverance.xml中添加:

<if-path path="/news/"> <replace css="html > head" content="content(html > head)" /> <replace css="html > body > div#content" content="content(html > body > div#content)" /> </if-path>

这样,当请求/news/archives时,Deliverance 先让 Plone 渲染页面,再用自己的规则重写;当请求/news/2012/01/hello时,则先让 Zine 渲染,再重写。一个 Deliverance 实例,同时代理两个源头。

注意:Repoze 的 pipeline 是单向的,plonezine不能互相调用。曾有人试图在 Plone 的 Python Script 里用urllib2调用 Zine 的/api/posts,结果触发了循环代理(Plone → Deliverance → Zine → Deliverance → Plone),导致 500 错误。正确做法是让 Deliverance 统一拉取,Plone 和 Zine 都只做数据源。

4. 完整实操流程与逐行配置说明

4.1 环境准备:CentOS 6.5 + Python 2.7.3 的硬核搭建

所有操作均在 root 用户下进行,不使用虚拟环境(当时 virtualenv 还不成熟)。先确认系统基础:

# 检查系统版本 cat /etc/redhat-release # 输出:CentOS release 6.5 (Final) uname -m # x86_64 # 升级基础包 yum update -y yum install -y gcc gcc-c++ make sqlite-devel libxml2-devel libxslt-devel # 安装 Python 2.7.3(系统自带是 2.6.6) cd /tmp wget https://www.python.org/ftp/python/2.7.3/Python-2.7.3.tgz tar xzf Python-2.7.3.tgz cd Python-2.7.3 ./configure --prefix=/opt/python2.7 --enable-shared make && make install echo "/opt/python2.7/lib" >> /etc/ld.so.conf.d/python2.7.conf ldconfig /opt/python2.7/bin/python2.7 -V # 确认输出:Python 2.7.3

关键点:--enable-shared必须加上,否则后续的 lxml 编译会失败,报undefined symbol: PyUnicodeUCS2_FromString。这是 CentOS 6 的经典坑,因为系统 Python 是 UCS2 编译的,而源码编译的 Python 默认是 UCS4。

接着安装 setuptools 和 easy_install:

cd /tmp wget https://pypi.org/packages/source/s/setuptools/setuptools-0.6c11.tar.gz tar xzf setuptools-0.6c11.tar.gz cd setuptools-0.6c11 /opt/python2.7/bin/python2.7 setup.py install

此时/opt/python2.7/bin/easy_install就可用。注意:不要用pip,因为 pip 0.6.3 无法正确处理 Zine 的setup.py中的install_requires字段,会漏装Werkzeug

4.2 Plone 4.1 的定制化安装与皮肤配置

下载 Plone 4.1 Unified Installer:

cd /tmp wget https://launchpad.net/plone/4.1/4.1.6/+download/Plone-4.1.6-UnifiedInstaller-1.6.2.tgz tar xzf Plone-4.1.6-UnifiedInstaller-1.6.2.tgz cd Plone-4.1.6-UnifiedInstaller-1.6.2

修改install.sh,在PYTHON=...行后添加:

# 强制使用我们编译的 Python PYTHON=/opt/python2.7/bin/python2.7

然后安装:

./install.sh standalone --no-prompt --password admin123

安装完成后,进入 instance 目录:

cd /opt/plone/instance

创建自定义主题plonetheme.unified

mkdir -p src/plonetheme.unified/plonetheme/unified cd src/plonetheme.unified /opt/python2.7/bin/python2.7 bootstrap.py bin/buildout

buildout.cfg的关键部分:

[buildout] parts = instance unified-theme develop = src/plonetheme.unified [unified-theme] recipe = plone.recipe.theme theme-dir = ${buildout:directory}/src/plonetheme.unified/plonetheme/unified

src/plonetheme.unified/plonetheme/unified下创建profiles/default/css/unified.css,内容为:

/* 全局重置 */ body { font-family: "Helvetica Neue", Arial, sans-serif; } #portal-top { background: #2c3e50; color: white; } #content { max-width: 960px; margin: 0 auto; }

然后在 Plone 站点中启用该主题:访问http://localhost:8080/portal_skins→ “选择主题” → 选unified→ “保存”。

4.3 Zine 的源码编译与数据库初始化

克隆 Zine 源码(注意 commit hash):

cd /tmp git clone https://github.com/mitsuhiko/zine.git cd zine git checkout a1b3c4d

安装依赖(顺序不能错):

/opt/python2.7/bin/easy_install -U "Werkzeug==0.6.2" /opt/python2.7/bin/easy_install -U "Jinja2==2.5.5" /opt/python2.7/bin/easy_install -U "SQLAlchemy==0.6.6" /opt/python2.7/bin/easy_install -U "lxml==2.3.6" # 必须指定版本,新版 lxml 不兼容 /opt/python2.7/bin/easy_install -U "zine"

初始化数据库:

mkdir -p /var/zine cp zine/config.py /var/zine/zine.cfg sed -i 's|sqlite:////tmp/zine.db|sqlite:////var/zine/data.db|g' /var/zine/zine.cfg /opt/python2.7/bin/python2.7 -c "from zine.database import init_database; init_database('/var/zine/zine.cfg')"

启动 Zine 测试:

/opt/python2.7/bin/python2.7 zine/app.py --config /var/zine/zine.cfg # 访问 http://localhost:5000,确认能看到博客首页

4.4 Deliverance 的安装与规则部署

Deliverance 的 PyPI 包已下线,必须从源码安装:

cd /tmp wget https://github.com/repoze/deliverance/archive/0.4.1.tar.gz tar xzf 0.4.1.tar.gz cd deliverance-0.4.1 /opt/python2.7/bin/python2.7 setup.py install

创建 Deliverance 配置目录:

mkdir -p /var/deliverance /var/cache/deliverance cp /tmp/deliverance-0.4.1/examples/deliverance.xml /var/deliverance/

编辑/var/deliverance/deliverance.xml,填入前文所述的四段规则。特别注意<if-path>的闭合标签,Deliverance 对 XML 格式极其敏感,少一个</if-path>就会整个规则失效。

4.5 Repoze Pipeline 的最终组装与启动

创建/var/repoze/wsgi.ini

[app:plone] use = egg:Products.PloneSoftwareCenter#zope2 zope2-instance = /opt/plone/instance [app:zine] use = egg:zine#main config = /var/zine/zine.cfg [filter:deliverance] use = egg:deliverance#main rules = /var/deliverance/deliverance.xml cache-directory = /var/cache/deliverance timeout = 5 cache = 300 [pipeline:main] pipeline = egg:repoze.tm#tm egg:repoze.retry#retry egg:repoze.who#who deliverance plone zine [server:main] use = egg:Paste#http host = 0.0.0.0 port = 8000

安装 Paste:

/opt/python2.7/bin/easy_install -U "Paste==1.7.5.1"

启动服务:

/opt/python2.7/bin/paster serve /var/repoze/wsgi.ini

此时访问http://localhost:8000/news/,应该看到 Zine 的内容,但顶部有 Plone 的导航栏,底部有 Plone 的 CSS 样式。打开浏览器开发者工具,检查<head>中的<link>标签,确认href/++resource++unified.css;检查<nav id="main">,确认内容来自 Plone 的portal_nav

实操心得:启动时报ImportError: No module named repoze.tm,是因为repoze.tm需要单独安装:/opt/python2.7/bin/easy_install -U "repoze.tm==1.0a4"。这个版本号必须精确,新版repoze.tm依赖transaction>=2.0,而 Plone 4.1 锁定了transaction==1.1.1,会冲突。

5. 常见问题与排查技巧实录

5.1 Deliverance 规则不生效:九成是 MIME 类型惹的祸

现象:Zine 页面正常显示,但 Deliverance 的<replace>完全没反应,最终 HTML 里还是 Zine 的原始<head>

原因:Deliverance 只处理Content-Typetext/htmlapplication/xhtml+xml的响应。而 Zine 默认返回text/html; charset=utf-8,Deliverance 的 lxml 解析器会因charset=utf-8后缀报错。

解决方案:在 Zine 的zine/app.py中,修改make_app()函数,在return Response(...)前加:

response.headers['Content-Type'] = 'application/xhtml+xml'

或者更稳妥的做法,是在 Deliverance 的wsgi.ini中添加:

[filter:deliverance] use = egg:deliverance#main rules = /var/deliverance/deliverance.xml # 强制解析为 XHTML content-type = application/xhtml+xml

实测对比:加了content-type参数后,Deliverance 的日志从INFO deliverance: Skipping response (unknown content type)变为INFO deliverance: Processing response with rules

5.2 Plone 和 Zine 的会话不同步:用户登录后 Zine 不显示用户名

现象:Plone 登录后,访问/news/页面,右上角仍是“登录”,而不是“欢迎,张工”。

原因:repoze.who中间件只把身份信息注入到environ,但 Zine 的模板没有读取它。Zine 的base.html模板里写的是:

{% if g.user %}欢迎,{{ g.user.name }}{% else %}<a href="/login">登录</a>{% endif %}

g.user是 Zine 自己的全局变量,和repoze.who无关。

解决方案:在 Zine 的zine/app.py中,修改make_app(),在app.before_request回调中注入:

@app.before_request def inject_identity(): identity = request.environ.get('repoze.who.identity') if identity: g.user = type('User', (), {'name': identity['repoze.who.userid']})() else: g.user = None

这样g.user.name就能拿到 Plone 的用户名。注意:identity字典的 key 是repoze.who.userid,不是userid,这是repoze.who的固定约定。

5.3 Deliverance 的 document() 调用超时:Plone 接口响应慢导致页面白屏

现象:访问/news/时,浏览器长时间等待,最终返回 500 错误,Deliverance 日志显示URLError: <urlopen error timed out>

原因:Plone 的@@user-info接口在首次调用时要初始化 session,耗时超过 Deliverance 默认的 30 秒。

解决方案:在wsgi.ini中为deliverancefilter 添加超时参数:

[filter:deliverance] use = egg:deliverance#main rules = /var/deliverance/deliverance.xml timeout = 5

同时,在 Plone 的@@user-infoview 中,添加缓存头:

class UserInfoView(BrowserView): def __call__(self): self.request.response.setHeader('Cache-Control', 'public, max-age=300') return json.dumps({...})

这样 Deliverance 的document()会缓存 5 分钟,避免重复请求。

5.4 CSS 样式丢失:Plone 的 ++resource++ 路径 404

现象:页面有导航和内容,但字体、颜色全是浏览器默认样式。

原因:Deliverance 重写的 `<