builds

搭一条私有笔记到公开站点的最小发布链

一次发布链实验:用显式发布门、临时目录和原子替换隔开私有知识库与公开站点。

这个站点的内容来自私有笔记库,但站点仓库可能公开。两边不能靠“记得不要复制敏感文件”维持边界,所以我先做了一条很小的发布链:只有显式标记允许发布的笔记才能进入中间目录,站点只读取这个目录,永远不知道私有库的位置和结构。

第一版目标

我没有从复杂的同步服务开始,而是限定了四个可验收结果:

  • 未标记 publish: true 的笔记完全不可见;
  • frontmatter 缺字段时导出失败;
  • 正文不保留未发布笔记的链接关系;
  • 任意检查失败时,上一版导出结果保持不变。

最后一条决定了实现不能逐个覆盖正式目录。导出过程应该先写到同级临时目录,通过全部检查后再一次性替换。

中间契约比框架重要

导出结果使用普通 Markdown。下面的伪代码展示了主流程,省略了具体解析器和 Windows 文件替换细节:

export async function exportPublished(notes, destination) {
  const selected = notes.filter((note) => note.meta.publish === true);
  const staging = await createStagingDirectory(destination);

  try {
    for (const note of selected) {
      const article = transformToPortableMarkdown(note, selected);
      validateFrontmatter(article.meta);
      scanForPrivateData(article);
      await writeArticle(staging, article);
    }
    await replaceDirectoryAtomically(staging, destination);
  } catch (error) {
    await discardStagingDirectory(staging);
    throw error;
  }
}

站点端再用内容 schema 校验一次。这不是重复劳动:导出器验证“能否安全发布”,站点验证“能否按页面契约渲染”。两个检查位于不同信任边界,错误信息也面向不同操作者。

遇到的失败

第一次实现只在文件名中搜索邮箱和绝对路径,很快就发现代码块、Markdown 链接目标和附件元数据都会漏检。第二次把扫描放到 Markdown 转换之后,又发现未命中的 WikiLink 会泄露私有笔记标题。最终规则变成:先构建允许发布的 ID 集合,链接只在集合内解析;未命中目标降级为正文显示文本,并在导出报告中计数。

这条链仍不是完整产品。它缺少图片指纹、增量导出和详细审计报告,但已经把最危险的“整个库被意外复制”变成默认不可能发生的路径。下一步优化应该建立在这个边界上,而不是牺牲边界换速度。

(示例内容,用于站点骨架验证)