Plone与Zine WSGI集成:解耦博客功能的工程实践 1. 项目概述为什么要把 Plone 和 Zine 拆开再缝合Plone 是我干了十多年内容管理系统CMS项目的老伙计稳定、安全、权限模型扎实做企业内网、政务门户、知识库这类“重结构、强流程”的系统它几乎从不掉链子。但一说到博客——那种需要快速发布、支持 Pingback/Trackback、有丰富插件生态、作者能三分钟发完一篇带图带视频的轻量级内容生产场景——Plone 就像让一位穿正装的银行风控经理去跳街舞不是不能动是动作僵硬、节奏卡顿、观众看着累。原文里那句“Blogging isnt 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–2011Python Web 生态中最成熟、最契合需求的工具而且它们之间形成了严丝合缝的咬合关系。我们来一层层拆开看为什么是它们而不是别的。2.1 应用层Plone 与 Zine 的职能划分与边界设定Plone 在这里扮演的是“权威内容中枢”和“用户身份总控台”。它的核心价值在于其无与伦比的权限模型通过portal_membership和portal_groups实现的细粒度对象级权限、强大的内容类型定义能力通过 Dexterity 或 Archetypes以及成熟的审核工作流Review State。一个典型的使用场景是公司新闻稿由市场部编辑在 Plone 里撰写、提交经法务审核后发布而部门内部的“技术分享”、“读书笔记”这类非正式、高频次的内容则由工程师直接在 Zine 里发布。两者的数据完全隔离——Plone 的内容存于 ZODBZine 的内容存于 MySQL或 SQLite互不干扰。这种隔离带来的好处是灾难性的如果 Zine 因为某个插件崩溃了Plone 网站纹丝不动反之Plone 升级失败Zine 的博客依然可读可写。原文提到“ran into various issues of dependencies or incompatibilities”这正是传统“Plone 博客插件”模式的死穴——所有东西都挤在同一个 Zope 实例里一个包的版本冲突就能让整个站变白屏。而 Zine 的定位非常清晰它就是一个“内容发布终端”。它不需要理解 Plone 的权限也不需要访问 ZODB。它只需要一个干净的 MySQL 数据库MySQL-python是当时最稳定的驱动一个能渲染 Jinja2 模板的引擎Jinja22.1.0以及一个能处理 HTTP 请求的底层Werkzeug0.4。它甚至不关心自己部署在哪个端口因为最终对外暴露的是经过 Deliverance 统一包装后的/blog路径。这种“各司其职”的设计让系统的可维护性指数级提升。我后来在给一家高校图书馆做类似集成时就严格遵循了这个原则把 Plone 当作“数字特藏库”的管理后台把 Zine 当作“馆员博客”的前台连数据库连接字符串都放在不同的配置文件里彻底杜绝了交叉污染的可能。2.2 协议层Repoze.zope2 作为 WSGI 化的“翻译官”Zope 2.x 默认的ZPublisher是一个古老的、紧耦合的请求处理器它和 Zope 的核心深度绑定无法脱离 Zope 环境独立运行。而 WSGIWeb 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只要它能输出标准的 HTMLDeliverance 就能对它动刀。它的规则引擎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[classsidebar]/*切下来缝进 Plone 主题的左栏//td[idportal-column-one]/div把 Zine 的文章主体//div[classcontents]切下来塞进 Plone 主题的内容区children:#region-content把 Plone 主题里不需要的面包屑#portal-breadcrumbs和登录框//td[idportal-column-one]/div/div直接切除。这种“前端集成”模式完美避开了后端模板引擎的兼容性难题。Plone 用 Chameleon当时叫 ZPTZine 用 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.cmmiConfigure-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.zope21.0b1要求zope.interface3.5.0,3.6.0而zope.interface3.5.4又要求zope.schema3.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.zope21.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它会自动重启省去了手动CtrlC再启动的麻烦。更重要的是当debug True时cgitb中间件会把完整的错误堆栈打印在浏览器页面上而不是只写在日志里。我曾经遇到一个rules.xml解析失败的问题错误信息是lxml.etree.XMLSyntaxError: None非常模糊。开启 debug 后堆栈里明确指出了是第 15 行的href属性值缺少了引号这才得以快速定位。3.3 Rules.xml 的 XPath 选择器实战与“视觉缝合”心法rules.xml是 Deliverance 的灵魂而 XPath 就是它的语言。原文中的 XPath 表达式如//div[classsidebar]/*和//td[idportal-column-one]/div看起来很直观但在真实环境中它们往往成为最顽固的“绊脚石”。原因在于Plone 和 Zine 的 HTML 结构会随着版本更新、主题更换、甚至仅仅是启用了某个插件而发生细微变化。classsidebar这个条件在 Zine 0.1.2 中是成立的但在 Zine 0.1.3 中开发者可能把它改成了classzine-sidebar。这时append theme... content//div[classsidebar]/* /就会失效Zine 的侧边栏不会出现在 Plone 页面上整个博客看起来就像一个没穿衣服的裸体。解决这个问题的心法是永远不要相信 class 名而要相信结构和语义。我推荐的替代方案是在 Zine 的模板里给关键区域添加一个独一无二的id。比如在 Zine 的layout.html模板中把侧边栏的div classsidebar改成div idzine-sidebar classsidebar。然后在rules.xml中把content属性改成//div[idzine-sidebar]/*。id是全局唯一的只要你不主动去改它它就永远不会变。同理对于 Plone 的目标区域也不要死磕#portal-column-one因为这个 ID 在 Plone 的不同皮肤Skin下可能不存在。更好的办法是利用 Plone 的viewlet机制。Plone 的内容区域通常是由#content-core这个 viewlet 容器承载的。所以append themechildren:#content-core contentchildren://div[idzine-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_passwordlocalhost/zine_db?charsetutf8 [application] name My Company Blog author IT Department email itcompany.com timezone UTC language en这里的uri是关键。mysql://是协议zine_user:zine_password是数据库用户名和密码localhost是数据库地址zine_db是数据库名。?charsetutf8这个参数是必须的否则中文标题和内容会变成乱码。创建数据库的 SQL 命令是CREATE DATABASE zine_db CHARACTER SET utf8 COLLATE utf8_general_ci; GRANT ALL PRIVILEGES ON zine_db.* TO zine_userlocalhost 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} SQLAlchemy0.5.0 Jinja22.1.0 Werkzeug0.4 simplejson html5lib pytz Babel0.9.4 lxml2.0 Pygments0.9 docutils0.5 zinebuildout0.4 MySQL-python [zine] recipe zc.recipe.cmmi url http://zine.pocoo.org/releases/Zine-0.1.2.tar.gz extra_options --pythonpython特别注意[zine-deps]中的extra-paths parts/zine/lib/zine。这个配置告诉 buildoutZine 的 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 version1.0 encodingUTF-8? ruleset !-- 主题来源从 Plone 站点获取完整的 HTML 骨架 -- theme href/zope/Plone/main_template / !-- 复制 Zine 的 head 内容到 Plone 主题的 head 中 -- rule prepend theme//head content//head/link nocontentignore / prepend theme//head content//head/style nocontentignore / append theme//head content//head/script nocontentignore / append theme//head content//head/meta nocontentignore / replace theme//head/title content//head/title nocontentignore / !-- 移除 Zine 特有的、与 Plone 无关的元素 -- drop content//div[idreddit] / drop content//div[classzine-footer] / !-- 移除 Plone 主题中不需要的元素 -- drop theme#portal-breadcrumbs / drop theme//div[idportal-searchbox] / drop theme//div[idportal-personaltools] / !-- 将 Zine 的侧边栏ID 为 zine-sidebar放入 Plone 的左栏 -- append theme//div[idportal-column-one]/div content//div[idzine-sidebar]/* nocontentignore / !-- 将 Zine 的主要内容ID 为 zine-main放入 Plone 的内容区 -- append themechildren:#content-core contentchildren://div[idzine-main] nocontentignore / !-- 将 Zine 的管理链接放入 Plone 的右栏 -- prepend themechildren:#portal-column-two content//div[idzine-admin-links]/ul nocontentignore / /rule /ruleset4.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文件变动时自动重启极大提升开发效率。服务启动后你应该能看到类似这样的日志