返回更新日志

版本更新

Syzygy 官网的内容平台:把文章当代码构建

官网博客、更新日志与指南由一个共享的静态内容平台生成:zod 严格校验 frontmatter,中英同 slug 配对,构建期生成 sitemap、RSS 与搜索索引,全程没有 CMS 和数据库。

Syzygy 团队发布于 阅读约 7 分钟
  • #内容平台
  • #静态站点
  • #SEO
  • #双语
  • #内容工程
本文目录

8 月 31 日凌晨的一次提交,给 syzygysync.com 加上了博客、更新日志和指南三种内容页,同一次构建里生成了 RSS 订阅、站点搜索索引和更新的 sitemap。首批上线的是七篇双语产品文章、一篇快速上手指南,以及你现在读的这篇更新日志。在这之前,官网有产品主页、定价、下载页和给 AI 爬虫看的 llms.txt,但文章本身没有家:导航里的入口是断链,搜索引擎能抓到的只有营销页。

这篇日志记录这个内容平台的工程决策。它同时服务于 Syzygy 产品站和 Astrolix 公司站,两个站点共用同一个内容平台包,各自持有自己的文章目录。选型的核心判断只有一条:内容即代码,构建即发布。文章是 Git 里的 Markdown 文件,Docker 构建阶段完成扫描、校验并产出全部静态页面;不引入博客 API、CMS,也不引入数据库。

为什么不接一个 CMS

先看约束。Astrolix 公司站没有 API,一个动态接口都没有。如果内容发布依赖运行时服务,公司站要么凭空长出一套后端,要么永远发不了文章。Syzygy 产品站有服务端,但那套服务负责账号、授权、激活码和配对授权,让它顺带管博客,等于把公开内容和商业状态机耦在一起。

静态构建的回报是具体的。爬虫拿到的是稳定完整的 HTML,不依赖 JS 渲染;页面可以被 CDN 缓存;整个官网镜像可以离线部署、整体回滚;每篇文章的修改都有 Git diff、review 记录和作者归属。发布一篇文章和发布一个版本的流程完全一样:提交、review、构建、上线。

我们也明确划了一条安全线:构建过程不抓取任何远程网页或图片。这保持构建可重复,也避免把发布流程变成一个 SSRF 风险面。外链图片必须用稳定的 HTTPS 地址,正式文章优先提交可审计的本地资源。

Frontmatter 是合同,不是建议

内容平台包用 zod 定义了一份严格 schema,每个 Markdown 文件的 frontmatter 都要过这一关:

  • title、description、slug、pubDate、author、category、lang 必填;
  • slug 只允许小写字母、数字和连字符;
  • pubDate 必须是 YYYY-MM-DD 格式;
  • lang 只能是 en 或 zh;
  • schema 启用了严格模式,frontmatter 里出现任何未知字段都算错误。

关键在于错误处理的位置:解析失败、字段非法、slug 在同类型同语言下重复、相对路径的封面在文章目录里解析不到真实文件,这些全部在构建期抛出异常,构建失败。错误不会等到某个线上页面渲染时才被发现。草稿执行同样的纪律:draft: true 的文章不进列表、不进静态路由参数、不进 sitemap、不进 RSS、不进搜索索引,metadata 里还会输出 noindex, nofollow。草稿留在 Git 里审阅,但不随公开镜像发布。

中英同 slug,构建期生成语言关联

双语是同一个 slug 的两个语言变体:p2p-pairing-sync.md 和 p2p-pairing-sync.zh.md 在构建后成为一对关联页面。语言对允许不对称:某一侧暂缺时构建照常通过,alternate 只会生成实际存在的那几侧,缺的一侧以后补上即可,路由和索引自动跟上。因为仓库里每篇文章都在加载时被解析成带类型、语言、日期的结构化记录,构建器可以机械地生成:

  • hreflang alternate(en-US、zh-CN,以及指向默认语言的 x-default);
  • 每个页面自己的 canonical 地址;
  • Open Graph 的 article 类型元数据,含发布时间、修改时间、作者与标签;
  • 同类型文章的上一篇 / 下一篇导航。

站点角色也是构建期决定的。www.syzygysync.com 是 Global 规范域名,默认英文;cn.syzygysync.com 是中国站,默认中文并承载备案语义;en.syzygysync.com 只保留给旧书签和旧客户端,不再生成新链接。页面语言按「URL 语言 > 用户保存的选择 > 浏览器语言 > 构建默认值」解析,Global 与中国的定价隔离由域名和部署目标负责,不靠按 IP 改写 canonical 这种隐式行为。

构建期产出的不止是 HTML

除了页面本身,构建还生成三样东西。sitemap.xml 只包含已发布文章。rss.xml 用经过转义的 XML 逐篇输出,避免标题或摘要里的特殊字符破坏订阅源。/search-index.json 面向站内搜索,每条记录带标题、摘要、URL、类型、分类、标签,以及一段从正文提取的纯文本,提取器会剥掉代码块、图片、链接语法和 Markdown 标记符号,只留可检索的词。

加载器本身是一个覆盖文件的纯函数:扫描 blog、changelog、guides 三个目录下的 .md 与 .mdx,排序规则是发布日期倒序、同日期按标题,列表页、RSS 与搜索索引共用同一份排序结果。内容目录之外出现文章文件会直接报错,类型是路径的第一个片段决定的,没有约定之外的灰色地带。

正文渲染的边界也写进了文档:图片用标准 Markdown 语法引用,输出带懒加载的 <img>;每张图必须有有意义的 alt,加载失败时正文仍应保留标题、说明和链接上下文,图片不能成为唯一的信息载体。SVG 适合架构图与流程图,可以直接内链引用。

文章里的图片也有一道构建门禁:一个独立脚本扫描所有 Markdown 的图片引用(Markdown 语法和 HTML <img> 都算),引用的本地路径必须真实存在,否则构建失败。图片要先放进公开资源真源目录再同步进应用,因为应用内的 public/ 目录是构建产物副本,每次构建会被清空重建,只把图存在那里会导致本地预览正常、正式产物缺图。这个坑我们踩过一次,然后用门禁把它变成不会再发生。

现在的边界

这套平台刻意保持小。渲染器是 react-markdown 加 remark-gfm,支持 GFM 表格、任务列表和代码块,图片输出懒加载的 <img>。任意 React 组件进正文的能力目前没有开放:.mdx 扩展名可以被发现,但内容需要保持 Markdown 兼容,等真的需要组件白名单、构建隔离和对应测试时再评估 MDX 工具链。

没有管理后台也是刻意的选择。如果未来需要编辑器,它会以内容生产后台的形态出现:编辑器产出 Markdown,发布动作落成 Git 提交或受控构建触发,而不是让公开页面直接读数据库。API 继续负责账号、权益、支付这些动态能力,内容发布保持可审计、可回滚、可脱离运行时服务。

这套边界由测试与门禁钉住。内容平台包自带单元测试覆盖校验失败路径与语言关联规则,两个站点的构建门禁都包含类型检查、lint 和完整构建;frontmatter、slug 或封面有问题,第一道反应是构建失败,永远不会是线上页面悄悄渲染出错的内容。

你现在读的这篇更新日志,就是这条流水线的第一个更新日志类产物;首批随平台上线的七篇双语文章是产品介绍、剪贴板内容类型、索引搜索与 PDF OCR、OCR 翻译与文本工作台、P2P 配对同步、预览弹窗与命令面板、标签与回收站。下一篇是第一个稳定版 1.0.0 的完整叙事。

常见问题

Syzygy 官网的内容平台用了 CMS 吗?

没有。文章是 Git 里的 Markdown 文件,Docker 构建阶段完成扫描、校验并产出静态 HTML、sitemap、RSS 与搜索索引,不引入博客 API、CMS 或数据库。

中英文文章如何关联?

同一内容类型下中英版本共用相同 slug、不同的 lang 字段。构建时据此生成 hreflang alternate、规范链接与语言路由,草稿与未发布内容不会进入 sitemap、RSS 和搜索索引。

frontmatter 写错了会怎样?

构建直接失败。未知字段、非法 slug、日期格式错误、本地封面文件不存在、slug 重复都会在构建期报错,错误不会被推迟到线上页面。