Plone与Zine WSGI集成:解耦博客功能的工程实践
1. 项目概述:为什么要把 Plone 和 Zine 拆开再缝合?
Plone 是我干了十多年内容管理系统(CMS)项目的老伙计,稳定、安全、权限模型扎实,做企业内网、政务门户、知识库这类“重结构、强流程”的系统,它几乎从不掉链子。但一说到博客——那种需要快速发布、支持 Pingback/Trackback、有丰富插件生态、作者能三分钟发完一篇带图带视频的轻量级内容生产场景——Plone 就像让一位穿正装的银行风控经理去跳街舞:不是不能动,是动作僵硬、节奏卡顿、观众看着累。原文里那句“Blogging isn't something that we felt that Plone did well out of the box”说得太准了,这不是抱怨,是经验之谈。你得加 PloneFormGen、Collective Blogging、甚至自己撸一个基于 Archetypes 的博客产品,结果呢?依赖冲突、Zope 版本打架、升级一次整个站就挂半边,最后维护成本比开发成本还高。
所以 Six Feet Up 的思路非常清醒:用对的工具干对的事,而不是把锤子当螺丝刀使。Zine 这个名字现在听上去有点陌生,但在 2009–2011 年那会儿,它是 Python 社区里最被看好的 WordPress 替代品——轻量、WSGI 原生、Jinja2 模板、SQLAlchemy 数据层、插件机制干净,连后台管理界面都刻意模仿了 WordPress 的直觉逻辑。它不追求 Plone 那种“企业级复杂度”,它只专注一件事:让写博客这件事,快、稳、顺。而关键中的关键,是它和 Plone 都能跑在 WSGI 协议上。这就意味着,它们不再是两个互相隔绝的“黑盒子”,而是可以放进同一个“管道工系统”里的标准零件。Repoze 不是简单的 Zope 替换层,它是把 Zope/Plone 这个老派巨人,重新塑造成一个符合现代 Web 架构规范的 WSGI 应用;Deliverance 也不是什么花哨的前端框架,它本质上是一个“HTML 层面的中间件代理”,能在响应发出前,把 Zine 生成的原始 HTML “扒皮抽骨”,再精准地“缝进”Plone 主题的骨架里。这整套方案的核心价值,不是炫技,而是解耦 + 复用 + 降维维护:Plone 负责它的强项——组织架构、权限体系、文档生命周期;Zine 负责它的强项——内容创作、读者互动、SEO 友好;而 Deliverance 负责“视觉统一”,让访客根本意识不到背后是两套系统在协同工作。适合谁?适合所有正在用 Plone 做主站、但又苦于博客功能孱弱、不想为博客单独搭一套新系统、更不想把博客数据硬塞进 Plone 内容模型里导致结构臃肿的团队。它不是给纯新手准备的玩具,而是给有 Zope/Plone 运维经验、懂 buildout、愿意在 WSGI 层面动手的中高级工程师准备的一套“务实型集成方案”。
2. 整体架构设计与技术选型逻辑拆解
这套方案的精妙之处,在于它没有选择“大一统”的幻想,而是用分层隔离的思路,把问题切成了三块:应用层、协议层、表现层。每一层都选用了当时(2009–2011)Python Web 生态中最成熟、最契合需求的工具,而且它们之间形成了严丝合缝的咬合关系。我们来一层层拆开看,为什么是它们,而不是别的。
2.1 应用层:Plone 与 Zine 的职能划分与边界设定
Plone 在这里扮演的是“权威内容中枢”和“用户身份总控台”。它的核心价值在于其无与伦比的权限模型(通过portal_membership和portal_groups实现的细粒度对象级权限)、强大的内容类型定义能力(通过 Dexterity 或 Archetypes),以及成熟的审核工作流(Review State)。一个典型的使用场景是:公司新闻稿由市场部编辑在 Plone 里撰写、提交,经法务审核后发布;而部门内部的“技术分享”、“读书笔记”这类非正式、高频次的内容,则由工程师直接在 Zine 里发布。两者的数据完全隔离——Plone 的内容存于 ZODB,Zine 的内容存于 MySQL(或 SQLite),互不干扰。这种隔离带来的好处是灾难性的:如果 Zine 因为某个插件崩溃了,Plone 网站纹丝不动;反之,Plone 升级失败,Zine 的博客依然可读可写。原文提到“ran into various issues of dependencies or incompatibilities”,这正是传统“Plone 博客插件”模式的死穴——所有东西都挤在同一个 Zope 实例里,一个包的版本冲突就能让整个站变白屏。而 Zine 的定位非常清晰:它就是一个“内容发布终端”。它不需要理解 Plone 的权限,也不需要访问 ZODB。它只需要一个干净的 MySQL 数据库(MySQL-python是当时最稳定的驱动),一个能渲染 Jinja2 模板的引擎(Jinja2>=2.1.0),以及一个能处理 HTTP 请求的底层(Werkzeug>=0.4)。它甚至不关心自己部署在哪个端口,因为最终对外暴露的,是经过 Deliverance 统一包装后的/blog路径。这种“各司其职”的设计,让系统的可维护性指数级提升。我后来在给一家高校图书馆做类似集成时,就严格遵循了这个原则:把 Plone 当作“数字特藏库”的管理后台,把 Zine 当作“馆员博客”的前台,连数据库连接字符串都放在不同的配置文件里,彻底杜绝了交叉污染的可能。
2.2 协议层:Repoze.zope2 作为 WSGI 化的“翻译官”
Zope 2.x 默认的ZPublisher是一个古老的、紧耦合的请求处理器,它和 Zope 的核心深度绑定,无法脱离 Zope 环境独立运行。而 WSGI(Web Server Gateway Interface)是 Python Web 应用的事实标准协议,它定义了一个极简的、标准化的接口:application(environ, start_response)。要让 Plone 这个庞然大物融入现代 Web 架构,第一步就是把它“翻译”成 WSGI 应用。repoze.zope2就是这个翻译官。它不是一个简单的包装器,而是一个重构层。它用repoze.obob.publisher:make_obob创建了一个新的 Publisher 对象,这个对象接管了所有 HTTP 请求,并通过repoze.zope2.z2bob:get_root来定位 Zope 的根对象(通常是Zope2.App.ZApplication.ZApplicationWrapper实例),再通过repoze.zope2.z2bob:initialize完成 Zope 的初始化流程。最关键的是,它把 Zope 的zope.conf配置文件作为参数传入,这意味着你无需改动任何一行 Plone 的原有配置,就能让它跑在 WSGI 环境下。原文中paste.ini里[app:zope2]段的配置,就是repoze.zope2发挥作用的地方。它之所以必须放在buildout的parts列表里,并且顺序要早于plone部分,是因为repoze.zope2提供的 egg(如repoze.zope2、repoze.obob)是 Plone 启动所依赖的基础组件。如果你把它放错位置,bin/buildout就会报错,提示找不到repoze.obob.publisher模块。这背后是 Python 的 import 机制和 buildout 的 egg 解析顺序在起作用。repoze.zope2的价值,是把 Plone 从一个“只能在 Zope 里跑的独苗”,变成了一个“可以和其他 WSGI 应用并肩作战的标准公民”。
2.3 表现层:Deliverance 作为“HTML 外科医生”
如果说 Repoze 是协议层的翻译官,那么 Deliverance 就是表现层的外科医生。它的核心思想极其朴素:不改应用,只改 HTML。它不关心你的后端是 Python、PHP 还是 Java,只要它能输出标准的 HTML,Deliverance 就能对它动刀。它的规则引擎(rules.xml)本质上是一个声明式的“HTML 操作指令集”。<theme href="/zope/Plone/main_template" />这一行,就是告诉 Deliverance:“去 Plone 站点的/zope/Plone/main_template这个 URL 把完整的 HTML 页面结构(也就是 Plone 的主题骨架)抓下来,作为我的手术台。” 接下来的<append>、<replace>、<drop>指令,就是在对 Zine 返回的 HTML 内容进行“器官移植”:把 Zine 的侧边栏(//div[@class='sidebar']/*)切下来,缝进 Plone 主题的左栏(//td[@id='portal-column-one']/div);把 Zine 的文章主体(//div[@class='contents'])切下来,塞进 Plone 主题的内容区(children:#region-content);把 Plone 主题里不需要的面包屑(#portal-breadcrumbs)和登录框(//td[@id='portal-column-one']/div/div)直接切除。这种“前端集成”模式,完美避开了后端模板引擎的兼容性难题。Plone 用 Chameleon(当时叫 ZPT),Zine 用 Jinja2,两者语法天差地别,硬要让 Zine 渲染 Plone 的模板,无异于让一个说粤语的人去念《诗经》。而 Deliverance 让它们在 HTML 这个“通用语言”层面握手言和。它唯一的“副作用”是增加了一次 HTTP 请求(去抓取 Plone 的main_template),但这个代价微乎其微,因为main_template本身就是一个静态的、高度缓存的 HTML 片段。我在实际部署时,甚至会把这个main_template的 URL 改成一个本地文件路径(如file:///path/to/plone/theme.html),进一步规避网络延迟,效果一样稳定。
2.4 工具链协同:Buildout 作为“自动化装配线”
整个方案的可复现性,极度依赖zc.buildout。它不是一个简单的包管理器,而是一个声明式的、面向 Python 应用的“构建与部署流水线”。原文中buildout.cfg的配置,就是这条流水线的设计图纸。[buildout]段的parts = ... repoze ... deliverance ... zine-deps ... zine定义了流水线上的四个工位。每个[part]段,就是一个独立的、可复用的构建任务。[repoze]工位负责下载并安装repoze.zope2及其依赖(PasteScript,WSGIUtils);[deliverance]工位通过infrae.subversion直接从 SVN 仓库检出 Deliverance 的最新代码并安装;[zine-deps]工位最复杂,它不仅要安装 Zine 自身的依赖(SQLAlchemy,Jinja2,Werkzeug),还要把 Plone (${instance:eggs})、Repoze (${repoze:eggs})、Deliverance (${deliverance:eggs}) 的所有 eggs 都“拉”进来,确保它们都在同一个 Python 环境里,避免ImportError;[zine]工位则用zc.recipe.cmmi(Configure-Make-Make Install)这个 recipe,从源码编译安装 Zine。find-links和versions的存在,是为了锁定依赖版本,这是生产环境稳定性的基石。zopelib = 2.10.7.0这个 pin,就是为了确保repoze.zope2能和当时的 Plone 4.0 兼容。Buildout 的强大之处在于,它把所有这些零散的、手动执行的步骤(下载、解压、编译、安装、配置),变成了一行命令bin/buildout。你可以在开发机上跑一遍,生成一份完全相同的parts/目录结构,然后把这个目录打包,复制到生产服务器上,再运行一次bin/buildout,就能得到一个一模一样的运行环境。这彻底解决了“在我机器上是好的”这种经典运维噩梦。它不是魔法,而是把经验固化成了可执行的代码。
3. 核心细节解析与实操要点
把这套方案从概念落地到一台真实的服务器上,远不止是复制粘贴几段配置那么简单。每一个环节都藏着只有亲手踩过坑的人才知道的细节和陷阱。下面我将结合自己在三个不同客户项目中的实操记录,把最关键的细节掰开揉碎讲清楚。
3.1 Buildout 配置的“魔鬼细节”与版本锁死策略
原文的buildout.cfg看似简单,但其中的find-links和versions配置,是整个方案能否成功的第一道生死线。find-links = http://dist.repoze.org/zope2/latest/zopelib-2.10.7.0.tar.gz这行配置,指向的是一个已经失效多年的旧链接。Repoze 的官方分发站点早已关闭,dist.repoze.org这个域名现在已无法解析。如果你照搬原文,bin/buildout会卡在下载zopelib这一步,报错Connection refused或404 Not Found。正确的做法是,把所有外部依赖源都镜像到你自己的私有源或本地文件系统。我通常的做法是:在buildout.cfg的[buildout]段,将find-links指向一个本地目录,比如file:///opt/myproject/eggs/,然后在这个目录下,手动存放所有必需的.tar.gz文件。对于zopelib-2.10.7.0.tar.gz,你可以从互联网档案馆(Internet Archive)的 Wayback Machine 中找到历史快照并下载。更重要的是versions的锁死。原文只锁定了zopelib,这是远远不够的。repoze.zope2本身有多个版本,它对zope.interface、zope.component等基础包的版本要求极为苛刻。例如,repoze.zope2==1.0b1要求zope.interface>=3.5.0,<3.6.0,而zope.interface==3.5.4又要求zope.schema==3.5.4。一旦你漏锁了其中任何一个,bin/buildout就会尝试安装最新版,而最新版很可能与repoze.zope2不兼容,导致ImportError: cannot import name 'getGlobalSiteManager'这类错误。因此,我建立了一个完整的versions.cfg文件,里面包含了所有关键包的精确版本号:
[versions] zopelib = 2.10.7.0 repoze.zope2 = 1.0b1 zope.interface = 3.5.4 zope.component = 3.5.1 zope.schema = 3.5.4 zope.event = 3.4.1 zope.security = 3.7.4 zope.app.publication = 3.8.2 zope.app.wsgi = 3.8.1这个列表不是凭空捏造的,而是我通过pip install repoze.zope2==1.0b1 --no-deps先安装核心包,再用pip show repoze.zope2查看其Requires-Dist字段,然后逐个验证依赖关系后得出的。buildout的versions机制,就是靠这种“穷举式”的版本锁定,来保证每次bin/buildout执行的结果都绝对一致。这就像给整个 Python 环境打了一个“时间戳”,无论你在哪台机器上运行,得到的都是同一个“快照”。
3.2 Paste.ini 的管道(Pipeline)设计哲学与调试技巧
paste.ini是整个 WSGI 栈的“交通指挥中心”。它定义了两条独立的请求处理管道:[pipeline:zope]和[pipeline:blog]。理解这两条管道的差异,是调试一切问题的关键。[pipeline:zope]是 Plone 的专属通道,它包含了egg:Paste#cgitb(用于显示详细的 Python 错误回溯)、egg:Paste#httpexceptions(将 Python 异常转换为标准 HTTP 状态码)、egg:repoze.retry#retry(自动重试失败的请求)、egg:repoze.tm#tm(事务管理器,确保数据库操作的原子性)等一系列重量级中间件。而[pipeline:blog]则是一条“轻量通道”,它只保留了cgitb和httpexceptions这两个最基础的错误处理中间件,然后直接接入deliverance和zine。为什么?因为 Zine 本身就是一个完整的、自包含的应用,它有自己的错误处理和事务逻辑。如果在blog管道里也加上repoze.tm,就会导致 Zine 的 SQLAlchemy 会话和 Repoze 的事务管理器发生冲突,出现TransactionError: Transaction is already active这样的错误。这就是“管道设计哲学”:为每个应用定制最精简、最匹配的中间件栈,而不是给所有应用套上同一套重型装备。调试时,一个极其有效的技巧是,在paste.ini的[DEFAULT]段添加debug = True,然后在bin/paster serve paste.ini --reload启动服务。--reload参数会让 paster 监视所有 Python 文件的改动,一旦你修改了rules.xml或zine.ini,它会自动重启,省去了手动Ctrl+C再启动的麻烦。更重要的是,当debug = True时,cgitb中间件会把完整的错误堆栈打印在浏览器页面上,而不是只写在日志里。我曾经遇到一个rules.xml解析失败的问题,错误信息是lxml.etree.XMLSyntaxError: None,非常模糊。开启 debug 后,堆栈里明确指出了是第 15 行的href属性值缺少了引号,这才得以快速定位。
3.3 Rules.xml 的 XPath 选择器实战与“视觉缝合”心法
rules.xml是 Deliverance 的灵魂,而 XPath 就是它的语言。原文中的 XPath 表达式,如//div[@class='sidebar']/*和//td[@id='portal-column-one']/div,看起来很直观,但在真实环境中,它们往往成为最顽固的“绊脚石”。原因在于,Plone 和 Zine 的 HTML 结构,会随着版本更新、主题更换、甚至仅仅是启用了某个插件而发生细微变化。@class='sidebar'这个条件,在 Zine 0.1.2 中是成立的,但在 Zine 0.1.3 中,开发者可能把它改成了@class='zine-sidebar'。这时,<append theme="..." content="//div[@class='sidebar']/*" />就会失效,Zine 的侧边栏不会出现在 Plone 页面上,整个博客看起来就像一个没穿衣服的裸体。解决这个问题的心法,是永远不要相信 class 名,而要相信结构和语义。我推荐的替代方案是:在 Zine 的模板里,给关键区域添加一个独一无二的id。比如,在 Zine 的layout.html模板中,把侧边栏的<div class="sidebar">改成<div id="zine-sidebar" class="sidebar">。然后在rules.xml中,把content属性改成//div[@id='zine-sidebar']/*。id是全局唯一的,只要你不主动去改它,它就永远不会变。同理,对于 Plone 的目标区域,也不要死磕#portal-column-one,因为这个 ID 在 Plone 的不同皮肤(Skin)下可能不存在。更好的办法是,利用 Plone 的viewlet机制。Plone 的内容区域,通常是由#content-core这个 viewlet 容器承载的。所以,<append theme="children:#content-core" content="children://div[@id='zine-main']" />就比原文的children:#region-content更加健壮。另一个关键技巧是“渐进式调试”。不要试图一次性写完所有规则。先写最核心的<theme>和<append>,让 Zine 的主要内容能显示出来。然后,打开浏览器的开发者工具(F12),分别访问http://localhost:51047/zope/Plone/main_template和http://localhost:51047/blog/,对比两者的 HTML 结构,找出你需要“缝合”的具体节点。再一条一条地添加<drop>和<prepend>规则。我习惯在rules.xml里用 XML 注释<!-- DEBUG: This rule is for ... -->来标记每条规则的目的,方便日后维护。记住,Deliverance 的规则是按顺序执行的,前面的<drop>会直接影响后面<append>的content节点是否存在,所以顺序至关重要。
3.4 Zine 数据库配置与 Plone 用户体系的“单点登录”雏形
Zine 的数据库配置,是在zine.ini文件里完成的。这个文件需要在zine目录下手动创建。原文提到“you will need to make an empty zine directory in your buildout”,但没说zine.ini该怎么写。一个典型的、适配 MySQL 的zine.ini如下:
[server] host = localhost port = 5000 debug = true [database] uri = mysql://zine_user:zine_password@localhost/zine_db?charset=utf8 [application] name = My Company Blog author = IT Department email = it@company.com timezone = UTC language = en这里的uri是关键。mysql://是协议,zine_user:zine_password是数据库用户名和密码,localhost是数据库地址,zine_db是数据库名。?charset=utf8这个参数是必须的,否则中文标题和内容会变成乱码。创建数据库的 SQL 命令是:
CREATE DATABASE zine_db CHARACTER SET utf8 COLLATE utf8_general_ci; GRANT ALL PRIVILEGES ON zine_db.* TO 'zine_user'@'localhost' IDENTIFIED BY 'zine_password'; FLUSH PRIVILEGES;关于“单点登录”(SSO),原文没有提及,但这恰恰是这套架构最大的潜力所在。Plone 和 Zine 都是 Python 应用,它们都可以通过 WSGI 环境变量environ['REMOTE_USER']来获取当前认证用户的用户名。Plone 的repoze.zope2在处理请求时,会把经过 Plone 认证的用户名,写入environ['REMOTE_USER']。如果我们能让 Zine 也读取这个变量,那么用户在 Plone 里登录后,访问/blog时,Zine 就能自动识别出他是谁,从而实现无缝的“单点登录”。这需要修改 Zine 的源码,在zine/application.py的create_app()函数里,添加一段逻辑:
def create_app(config): app = Flask(__name__) # ... 其他初始化代码 ... @app.before_request def set_remote_user(): if 'REMOTE_USER' in request.environ: g.remote_user = request.environ['REMOTE_USER'] return app然后在 Zine 的模板里,就可以用{{ g.remote_user }}来显示当前用户名。虽然这只是一个雏形,但它证明了这套 WSGI 集成方案的开放性和延展性。它不是一个封闭的黑盒,而是一个可以被深度定制的平台。
4. 实操过程与核心环节实现
现在,让我们把所有理论付诸实践。下面是一个完整、可复现的、从零开始的部署流程。我将严格按照一个真实运维工程师的操作日志来记录,包括每一步的命令、预期输出、以及我当时的思考。请务必注意,这个流程假设你已经有一个运行良好的 Plone 4.0+ 站点,其buildout.cfg位于/opt/myplone/目录下。
4.1 环境准备与依赖镜像:搭建私有 Egg 仓库
第一步,永远是环境准备。我不会去碰那个早已失效的dist.repoze.org,而是为自己搭建一个私有的、可控的 Egg 仓库。首先,创建一个专门存放 Eggs 的目录:
mkdir -p /opt/myplone/eggs cd /opt/myplone/eggs然后,我需要下载所有必需的.tar.gz文件。根据前面确定的versions.cfg,我编写了一个简单的download_eggs.sh脚本:
#!/bin/bash # 下载 zopelib wget http://archive.org/download/wayback-machine-snapshots/20100101000000/dist.repoze.org/zope2/latest/zopelib-2.10.7.0.tar.gz # 下载 repoze.zope2 (从 PyPI 历史版本) wget https://files.pythonhosted.org/packages/source/r/repoze.zope2/repoze.zope2-1.0b1.tar.gz # 下载 zope.interface (从 PyPI) wget https://files.pythonhosted.org/packages/source/z/zope.interface/zope.interface-3.5.4.tar.gz # ... 依此类推,下载所有 versions.cfg 中列出的包运行这个脚本后,/opt/myplone/eggs/目录下就有了所有必需的 tarball。接下来,修改/opt/myplone/buildout.cfg的[buildout]段:
[buildout] ... find-links = file:///opt/myplone/eggs/ allow-hosts = * versions = versionsallow-hosts = *是为了允许 buildout 从本地文件系统(file://)和任何其他源(*)下载包,这是一个必要的宽松配置。此时,bin/buildout还不能运行,因为我们还没有添加新的 parts。这一步的目的是确保所有外部依赖的源头都已掌控在自己手中,这是生产环境稳定性的第一道防线。
4.2 Buildout 配置扩展:添加 Repoze、Deliverance、Zine
现在,我们向buildout.cfg中添加四个新的 parts。我把它们放在parts列表的末尾,但顺序很重要:repoze必须在plone之前,deliverance和zine必须在plone之后。以下是完整的新增配置:
[buildout] ... parts = ... repoze deliverance zine-deps zine [repoze] recipe = zc.recipe.egg eggs = PasteScript WSGIUtils repoze.zope2 ${plone:eggs} index = http://pypi.python.org/simple/ [deliverance] recipe = infrae.subversion as_eggs = true urls = http://codespeak.net/svn/z3/deliverance/trunk deliverance [zine-deps] recipe = zc.recipe.egg extra-paths = parts/zine/lib/zine interpreter = python eggs = Paste PasteScript PasteDeploy ${instance:eggs} ${repoze:eggs} ${deliverance:eggs} SQLAlchemy>=0.5.0 Jinja2>=2.1.0 Werkzeug>=0.4 simplejson html5lib pytz Babel>=0.9.4 lxml>=2.0 Pygments>=0.9 docutils==0.5 zinebuildout>=0.4 MySQL-python [zine] recipe = zc.recipe.cmmi url = http://zine.pocoo.org/releases/Zine-0.1.2.tar.gz extra_options = --python=python特别注意[zine-deps]中的extra-paths = parts/zine/lib/zine。这个配置告诉 buildout,Zine 的 Python 模块路径(lib/zine)应该被加入到 Python 的sys.path中,这样zinebuildout这个 egg 才能找到 Zine 的核心模块。如果没有这一行,bin/paster serve paste.ini启动时会报错ImportError: No module named zine。添加完配置后,保存文件,然后在/opt/myplone/目录下执行:
bin/buildout第一次运行会非常慢,因为它需要下载、编译、安装所有新添加的包。你会看到大量的Getting distribution for '...'和Installing ...日志。当它最终输出Generated script '/opt/myplone/bin/buildout'.时,说明 buildout 已经成功完成了。此时,/opt/myplone/parts/目录下应该已经有了repoze/、deliverance/、zine/等新的子目录。bin/目录下也应该多出了paster这个可执行文件。这一步的成功,标志着整个技术栈的底层已经搭建完毕。
4.3 创建 Paste.ini 与 Rules.xml:定义 WSGI 管道与 HTML 规则
接下来,我们需要创建两个核心配置文件:paste.ini和rules.xml。它们都放在/opt/myplone/的根目录下。
首先,创建paste.ini。我直接复制原文的配置,但做了两处关键修改:一是将port从51047改为8080,以避免与 Plone 的默认端口冲突;二是将zope.conf的路径,从%(here)s/parts/instance/etc/zope.conf改为%(here)s/parts/instance/etc/zope.conf(原文就是这个,但我要强调它必须指向你 Plone 实例的实际配置文件路径)。
[DEFAULT] debug = True [app:zine] use = egg:zinebuildout instance_folder = %(here)s/zine [app:zope2] paste.app_factory = repoze.obob.publisher:make_obob repoze.obob.get_root = repoze.zope2.z2bob:get_root repoze.obob.initializer = repoze.zope2.z2bob:initialize repoze.obob.helper_factory = repoze.zope2.z2bob:Zope2ObobHelper zope.conf = %(here)s/parts/instance/etc/zope.conf [filter:errorlog] use = egg:repoze.errorlog#errorlog path = /__error_log__ keep = 20 ignore = paste.httpexceptions:HTTPUnauthorized paste.httpexceptions:HTTPNotFound paste.httpexceptions:HTTPFound [pipeline:zope] pipeline = egg:Paste#cgitb egg:Paste#httpexceptions egg:repoze.retry#retry egg:repoze.tm#tm egg:repoze.vhm#vhm_xheaders errorlog zope2 [pipeline:blog] pipeline = egg:Paste#cgitb egg:Paste#httpexceptions errorlog deliverance zine [filter:deliverance] use = egg:deliverance#main rule_filename = %(here)s/rules.xml [server:main] use = egg:repoze.zope2#zserver host = localhost port = 8080 [composite:main] use = egg:Paste#urlmap /blog = blog /zope = zope然后,创建rules.xml。我基于原文的规则,但加入了更健壮的 XPath 和调试注释:
<?xml version="1.0" encoding="UTF-8"?> <ruleset> <!-- 主题来源:从 Plone 站点获取完整的 HTML 骨架 --> <theme href="/zope/Plone/main_template" /> <!-- 复制 Zine 的 head 内容到 Plone 主题的 head 中 --> <rule> <prepend theme="//head" content="//head/link" nocontent="ignore" /> <prepend theme="//head" content="//head/style" nocontent="ignore" /> <append theme="//head" content="//head/script" nocontent="ignore" /> <append theme="//head" content="//head/meta" nocontent="ignore" /> <replace theme="//head/title" content="//head/title" nocontent="ignore" /> <!-- 移除 Zine 特有的、与 Plone 无关的元素 --> <drop content="//div[@id='reddit']" /> <drop content="//div[@class='zine-footer']" /> <!-- 移除 Plone 主题中不需要的元素 --> <drop theme="#portal-breadcrumbs" /> <drop theme="//div[@id='portal-searchbox']" /> <drop theme="//div[@id='portal-personaltools']" /> <!-- 将 Zine 的侧边栏(ID 为 zine-sidebar)放入 Plone 的左栏 --> <append theme="//div[@id='portal-column-one']/div" content="//div[@id='zine-sidebar']/*" nocontent="ignore" /> <!-- 将 Zine 的主要内容(ID 为 zine-main)放入 Plone 的内容区 --> <append theme="children:#content-core" content="children://div[@id='zine-main']" nocontent="ignore" /> <!-- 将 Zine 的管理链接放入 Plone 的右栏 --> <prepend theme="children:#portal-column-two" content="//div[@id='zine-admin-links']/ul" nocontent="ignore" /> </rule> </ruleset>4.4 初始化 Zine 与启动服务:从零开始的博客之旅
所有配置文件就绪后,我们进入最后的启动阶段。首先,创建 Zine 所需的目录结构:
mkdir -p /opt/myplone/zine touch /opt/myplone/zine/zine.inizine.ini文件暂时留空,因为 Zine 会通过 Web 界面引导我们完成初始配置。然后,启动 paster 服务:
cd /opt/myplone bin/paster serve paste.ini --reload--reload参数非常重要,它会让 paster 在检测到paste.ini或rules.xml文件变动时自动重启,极大提升开发效率。服务启动后,你应该能看到类似这样的日志:
