Ghost 没有插件系统:我加的功能到底装在哪
很多人以为 Ghost 能像 WordPress 那样装插件。它不能——一个都没有。所以"我加的那些功能"到底装在哪,值得说清楚。
给博客加功能的时候,我被问过一个很合理的问题:
"你加的这些,是 Ghost 自带的,还是你作为插件植入的?"
答案有点出人意料:Ghost 根本没有插件系统。不是"我没用",是这个东西不存在。所以先把边界讲清楚——因为它决定了一件很实际的事:以后升级、换人接手,什么会坏、什么不会坏。
Ghost 5 的扩展点只有三个
打开它的内容目录,能扩展的地方就这么几个:
- 主题(
themes/):Handlebars 模板 + CSS + JS。官方允许你写任意前端代码。 - 存储适配器(
adapters/storage/):决定图片存哪。接对象存储、接 S3 都是走这里。 - 集成(Admin API):给外部程序一个带权限的口子。
目录里还有一个 apps/——那是早期版本遗留的机制,已经废弃,通常是个空文件夹。没有 plugins/,也没有插件市场。
知道这个前提之后,"我加的功能装在哪"就变成一个可以逐条回答的问题了。
把功能按来源分三类
一、标准件(Ghost 原生,只是配置了)
文章、标签页、页面、作者页、RSS、站点地图、分页——这些全是它自带的。导航栏用的是它自带的模板助手,你只是在后台填了几行"名称 + 链接"。
二、主题代码(官方扩展点,但不是插件)
我做的界面部分全在这里:整体样式、导航下拉菜单、搜索界面、首页轮播图。整个主题十几个文件,一个 CSS、一个 JS,其余是模板。
这些事情写在主题里,好处是:它属于官方兼容层。Ghost 升级大版本,主题不会被弄坏(除非它用了被移除的模板能力)。
三、外部件(Ghost 完全不知道)
这是最容易被忽略的一类。以站内搜索为例,它其实是三件东西拼起来的:
- 界面:主题里的一段 JS——点放大镜展开、输入、本地过滤。
- 数据:对象存储上的一个静态 JSON 文件,只含已发布文章。
- 生成:跑在服务器上的一个小脚本,定期通过 Ghost 的 API 拉文章、生成那个 JSON。
Ghost 全程不知道"搜索"这回事——它只是被读了一次文章列表。搜索发生在浏览器里,一次请求把索引拉下来,之后全是本地计算。
这么做有代价也有好处:代价是索引会有几分钟延迟;好处是零查询压力、零后端依赖,几十篇文章的规模下体验比"每敲一个字发一次请求"好得多。
顺带说一个身份问题:我确实有个"写文章助手"是真正的插件,但它的宿主不是 Ghost,而是我本地那个 AI 工具——它只是通过 Admin API 往 Ghost 里写内容。所以"插件"这个词用在这里,宿主搞错了。
为什么要分清:一张边界表
下面这张表,是我给自己(和以后接手的人)留的:
| 功能 | 属于 | 升级 Ghost 会不会坏 |
|---|---|---|
| 文章、标签、页面、RSS、站点地图 | Ghost 原生 | 不会 |
| 导航栏 | Ghost 原生助手 | 不会 |
| 界面样式、下拉菜单、搜索界面、轮播 | 主题代码(官方扩展点) | 不会 |
| 图片进对象存储 | 存储适配器(官方扩展点) | 要盯适配器跟不跟得上新版本 |
| 搜索索引 + 生成脚本 | 外部脚本 + 静态文件 | 不会(它只用 HTTP API) |
| 写文章的助手 | 另一个软件的插件,通过 API 操作 | 不受影响 |
表里唯一需要留意的,是那条存储适配器——因为图片存哪是它决定的,而它是个第三方包,得跟着 Ghost 的版本走。
如果非要说"绕过",只有两处
为了不给自己贴金,也把它们列出来:
- 改站点设置(图标、导航)是直接改数据库再重启的。因为那个 API 权限的口子不允许改设置——不是不能做,是那个 token 没这个权限。
- 索引脚本、部署脚本跑在服务器上,不在 Ghost 的生命周期里。Ghost 重启、升级,跟它们无关。
但这两处都没有改 Ghost 一行源码。这句话很关键:它意味着升级 Ghost 不会把定制弄丢。
踩过的坑(真要动手的话)
顺手把这几个真实的坑留在这里,都是这个站上踩出来的:
- 正文不一定在
html字段里。新版 Ghost 把内容存成结构化数据,html和纯文本字段可能都是空的。你要抽正文,得去读那个结构化字段;而且里面文本节点的类型名,跟你想的不一样。 - 别自己遍历导航。我想加二级菜单,就自己写循环去读导航数据,结果所有菜单项的链接都变成了首页——页面看着正常,点哪个都回首页,等于整条导航废掉。正确做法是用它自带的导航助手,二级菜单另外想办法加。
- 给文章挂标签,别用"名字"。用名字挂,系统会悄悄建出一批同名标签;等你清理重复标签时,可能把文章和标签的关系一起删掉,栏目页全变 404。要用不可变的 ID。
- 对象存储要单独配跨域规则。图片用
img标签加载不受影响,但用 JS 去读那个 JSON 会被浏览器拦。这是两套机制,别以为图片能显示就万事大吉。 - CSS 优先级能吃掉落样式。我给段落写了"上下相邻元素留白",又被一条更具体的"段落边距归零"盖掉了——结果所有段落挤在一起。选择器写得越具体,越容易出这种事。
一句话总结
加功能之前先问一句:这东西属于"标准件"还是"定制件"?想清楚这一句,升级的时候心里就有底——该放心的放心,该盯着的盯着。
界面能随便改,因为它们跑在官方兼容层里;真正需要照顾的,是那些"Ghost 不知道它存在"的部分——它们不出声,但也因此不会在升级时炸掉。