Skip to content

Quartz v5:从 Markdown AST 到数字花园静态站点的完整构建链

Quartz v5 是一套面向数字花园和关联笔记的静态站点生成框架。框架接收 Markdown、Obsidian 风格链接及附件,把内容交给可配置的转换器、过滤器、页面类型和发射器,最终生成可以直接托管的 HTML、CSS、JavaScript、索引和静态资源。部署后的站点不要求常驻 Quartz 服务,搜索、关系图、反向链接、弹出预览和单页导航等能力由构建产物与浏览器脚本共同完成。

从架构上看,Quartz 同时包含 Node.js 构建端和浏览器运行端。构建端负责读取 YAML 配置、装载社区插件、建立 Markdown AST 与 HTML AST 管线、生成普通页和虚拟页,再用 Preact 做服务端静态渲染;浏览器端接收已编排的组件脚本,通过统一的 nav 事件适配首次加载和 SPA 导航。Git 仓库、npm 包、Google Fonts、评论或分析服务属于按配置出现的外部依赖,不是每次离线构建都必需。

这套源码最值得抓住的并非某个页面组件,而是四层契约:内容怎样经过 Transformer、Filter、Emitter;同一份内容怎样由 Page Type 选择页面形态;布局怎样由 Component 和 Frame 组合;开发模式怎样在局部内容重建与框架硬重建之间分流。本文以 npx quartz build 为入口,走完一次静态构建,并展开插件装载、虚拟页、嵌入解析、资源哈希和热更新等代表性分支。

GitHub 仓库信息

信息内容
项目标题Quartz v5
项目描述一套快速、功能齐备的静态站点生成器,可将 Markdown 内容转换成完整网站。
GitHub 仓库jackyzha0/quartz
官网地址https://quartz.jzhao.xyz
主要开发语言TypeScript 72.29%、JavaScript 21.60%、SCSS 4.41%
开源许可证MIT
最近代码更新2026-08-18(GitHub pushed_at,核对日期:2026-09-02)

仓库结构与模块职责

Quartz 不是典型的多包 monorepo。核心框架、CLI、默认站点、文档和用户内容位于同一个仓库,社区插件则主要作为独立 npm 包或 Git 仓库装入。

text
quartz/
├── quartz/
│   ├── bootstrap-cli.mjs       CLI 进程入口和 Node 版本门
│   ├── cli/                    create、build、sync、plugin 等命令
│   ├── build.ts                全量构建与内容增量重建
│   ├── processors/             parse、filter、emit 三段内容管线
│   ├── plugins/
│   │   ├── loader/             YAML、插件、依赖、组件和 Frame 装载
│   │   ├── pageTypes/          页面匹配、虚拟页和调度器
│   │   └── emitters/           资源、静态文件等内置发射器
│   ├── components/             Preact 组件、布局 Frame 和页面渲染
│   ├── styles/                 基础样式与用户自定义样式
│   └── util/                   路径、日志、资源、主题等基础设施
├── content/                    默认内容输入目录
├── docs/                       Quartz 自身文档,也是可构建的示例站点
├── quartz.config.default.yaml  默认配置、插件及布局声明
├── quartz.ts                   TypeScript 配置覆盖入口
├── package.json                版本、命令、Node/npm 要求与依赖
└── public/                     默认构建输出目录,运行后生成
模块或核心文件负责什么与上下游的关系
bootstrap-cli.mjscli/handlers.js解析命令、编译 TypeScript 构建模块、启动预览服务和源码监听package.json#bin 接收命令,动态导入 build.ts 的编译结果
config-loader.ts解析配置,安装或导入插件,校验依赖与顺序,实例化四类插件和布局quartz.ts 顶层加载,结果成为整个构建上下文的 cfg
processors/parse.ts文本、mdast、hast 三阶段转换和 worker 线程调度接收内容路径,向 Filter 和 Emitter 提供 ProcessedContent
processors/filter.ts按顺序执行发布条件移除草稿、私有或不满足显式发布条件的内容
processors/emit.ts编排资源、页面和其他输出插件先准备资源与虚拟页,再并行运行其余 Emitter
pageTypes/dispatcher.ts选择页面类型,生成标签页、目录页、404 等虚拟页把布局、Frame、组件和页面 AST 交给 renderPage
components/renderPage.tsx展开嵌入、执行树转换、选择 Frame、用 Preact SSR 生成 HTML是页面类型与最终 .html 文件之间的渲染边界
componentResources.ts收集组件 CSS/JS,处理字体、哈希、压缩和 SPA 脚本顺序必须先于页面渲染,为 HTML 提供稳定的资源文件名

docs/、测试、GitHub Actions 和 Dockerfile 对理解运行方式有帮助,但本文不会把文档站点的每个功能页、所有社区插件实现或所有部署平台逐一展开。主分析范围集中在能证明框架构建主链的核心目录。

核心能力与实现机制

把 Markdown 变成可扩展的中间表示

Quartz 没有直接用字符串模板替换 Markdown。内容先进入 remark-parse 得到 mdast,再经过 Transformer 提供的 Markdown 插件,随后由 remark-rehype 转成 hast,最后执行 HTML 插件。文本预处理、Markdown 语义和 HTML 语义因此位于不同阶段,Obsidian wikilink、frontmatter、代码高亮或数学公式可以选择合适的 AST 层扩展,而不必挤在同一个正则处理器中。

用插件类别表达内容生命周期

v5 的处理插件主要分为 Transformer、Filter、Emitter 和 Page Type。四种类别分别回答“怎样解释内容”“是否发布”“还要生成什么文件”“这一类页面如何呈现”。一个插件可以同时属于多个类别,也可以附带组件或 Frame。配置加载器会合并 YAML 选项、manifest 默认值和 TypeScript 覆盖,再按 order 排序。收益是扩展点围绕内容生命周期组织;代价是最终行为取决于插件集合、加载顺序和跨插件依赖,排障不能只看单个插件。

普通页、虚拟页和页面类型共用渲染器

Markdown 文件会产生普通页面,标签、文件夹、Canvas、Bases 或 404 等页面可以由 Page Type 的 generate 生成虚拟内容。调度器先把所有虚拟页加入 allFiles,再填充其 htmlAst,因此普通笔记可以嵌入虚拟页,虚拟页之间也可以互相引用。匹配页面时按优先级遍历,命中第一个 Page Type 就停止,这使“谁先匹配”成为实际路由规则。

组件和 Frame 分离页面内容与页面骨架

Page Type 决定当前内容属于哪类页面,Component 提供标题、目录、图谱、搜索等局部能力,Frame 决定这些插槽如何构成整页骨架。布局配置可以针对 page type 覆盖左右栏、正文前后和 footer,并选择不同 Frame。这样 Canvas 可以采用全宽结构,普通笔记仍使用多栏布局,而外层 HTML、#quartz-root 和 SPA 协议保持一致。

静态站点也保留浏览器交互

构建结果是静态文件,但组件可以声明 beforeDOMLoadedafterDOMLoaded 脚本。Quartz 把这些资源统一收集,生产构建使用内容哈希,并确保 SPA router 最后加载:组件必须先注册 nav 监听器,路由器才能派发首次导航事件。静态托管与浏览器交互由此并存,不需要一个长期在线的应用服务器。

两级监听减少开发期等待

内容文件变化时,Quartz 维护 contentMap,只重新解析新增或修改的 Markdown,再重新过滤与发射;TypeScript、TSX、SCSS、配置或 package.json 变化时,则重新执行 esbuild、动态导入新的构建模块,属于硬重建。前者保留已解析内容,后者替换框架代码,两条路径解决的是不同变化成本。

源码环境准备与本地运行

本文固定阅读默认分支 v5075afd3 源码快照。仓库 package.json 的版本是 5.0.0,而 GitHub 最新公开 Release 仍是 2023 年 8 月发布的 v4.0.8。如果要复现本文链路,应检出固定提交,不要用 v4 Release 压缩包替代。

工具链与源码

package.jsonbootstrap-cli.mjs共同要求 Node.js 22 或更高版本,npm 要求不低于 10.9.2。

bash
git clone https://github.com/jackyzha0/quartz.git
cd quartz
git checkout 075afd3f712da0088a07f5284a7b3aba37dd61b6

node -v
npm -v
npm ci

node -v 至少应为 v22.xnpm ci 成功后才具备运行 CLI 的本地依赖。随后可按官方安装流程创建配置、安装插件并预览:

bash
npx quartz create
npx quartz plugin install --from-config
npx quartz build --serve

默认成功信号是终端出现 Started a Quartz server listening at http://localhost:8080,浏览器可以打开该地址;热更新另外使用 WebSocket 端口 3001。只构建而不启动预览时执行 npx quartz build,成功后应在 public/ 看到 HTML 与静态资源。

本次任务只下载并静态阅读了固定提交的源码、配置和官方文档,没有执行 npm ci、插件安装、构建、测试、开发服务器或 Docker。尤其要注意,插件安装与配置加载可能访问 npm 或 Git 仓库、写入 .quartz/plugins/、执行插件构建并安装原生依赖;在企业网络或审查环境中,应先核对 quartz.config.yamlquartz.lock.json,再允许这些步骤联网和执行。

整体架构地图

想找的功能首选源码位置阅读切入点
CLI 命令和预览服务quartz/bootstrap-cli.mjsquartz/cli/handlers.jsbuild 命令和 handleBuild
内容转换顺序quartz/processors/parse.tscreateMdProcessorcreateHtmlProcessorparseMarkdown
草稿或私有内容过滤quartz/processors/filter.ts 与对应 Filter 插件shouldPublish
插件来源、依赖和顺序quartz/plugins/loader/config-loader.tsloadQuartzConfigvalidateDependencies
标签页、目录页、Canvas、404quartz/plugins/pageTypes/PageTypeDispatcher 与各插件的 match/generate
页面嵌入与最终 HTMLquartz/components/renderPage.tsxrenderTranscludesrenderPage
布局骨架quartz/components/frames/resolveFrame、Frame registry
CSS、JS、字体和 SPAquartz/plugins/emitters/componentResources.tsComponentResources
内容热更新quartz/build.tsstartWatchingrebuild
框架源码热更新quartz/cli/handlers.jsesbuild context 与动态 import

CLI、配置装载与启动链

npx quartz build 到可执行构建模块

package.jsonbin.quartz 指向 quartz/bootstrap-cli.mjs。入口先检查 Node 主版本,再由 yargs 把 build 分派给 handleBuild。当带有 --serve 时,源码会自动开启 watch,所以预览模式天然包含内容与框架监听。

handleBuild 不直接解释全部 TypeScript。该函数创建 esbuild context,将 quartz/build.ts、TSX、SCSS 以及组件声明的 .inline.ts 浏览器脚本编译到 .quartz-cache/transpiled-build.mjs。随后使用带随机 query 的动态 import 绕过 Node 模块缓存,再调用默认导出的构建函数。这个边界很重要:CLI 是稳定的 JavaScript 启动壳,真正的构建框架可以在开发期被重新编译和替换。

配置在模块导入阶段完成装配

build.ts 顶层导入 ../quartz,而 quartz.ts立即调用 loadQuartzConfig()。因此配置与插件装载发生在构建模块被动态导入时,而不是解析第一篇 Markdown 时。

配置文件优先级由 resolveConfigPath确定:

text
quartz.config.yaml
  → 旧 quartz.plugins.json
  → quartz.config.default.yaml
  → 旧 quartz.plugins.default.json

加载器随后完成以下装配:

  1. 选出已启用插件;非 npm 来源会进入安装流程,并收集原生依赖。
  2. package.jsonquartz 字段或插件模块读取 manifest。
  3. 校验依赖是否存在、是否启用、顺序是否合法,并检测循环依赖。
  4. 将插件放入 Transformer、Filter、Emitter、Page Type 类别,按 order 排序。
  5. 合并 manifest 默认选项、YAML 选项和 TypeScript 覆盖,实例化插件。
  6. 装载插件提供的 Component 与 Frame,解析布局,最后追加 PageTypeDispatcher

插件依赖出错会阻断配置装载;某个插件安装、manifest 读取或实例化失败的部分路径只记录错误并继续,可能导致后续出现“插件未加载”而非最初的网络错误。调试时应从配置加载日志开始,而不是直接跳到 Markdown 解析器。

核心功能链路矩阵

链路入口关键决策输出或副作用
全量构建buildQuartz()ignore、slug、插件顺序清空并重建 public/
Markdown 转换parseMarkdown()单线程或 worker、Transformer 顺序ProcessedContent 的 hast 与元数据
发布过滤filterContent()每个 Filter 的 shouldPublish进入渲染的内容集合
页面生成PageTypeDispatcher.emit()Page Type 优先级与首个匹配普通页、标签页、目录页、404 等 HTML
页面嵌入renderTranscludes()整页、标题、块引用与循环检测展开后的页面树或循环提示
资源编排ComponentResources.emit()serve/production、SPA、字体来源哈希 CSS/JS、字体和脚本加载顺序
内容增量重建rebuild()add/change/delete、partialEmit更新受影响内容与派生产物
框架硬重建handleBuild() watcherTS/TSX/SCSS/config/package 变化重新编译并导入构建模块

全量构建:内容如何进入 public/

buildQuartz先创建 BuildCtx,再执行一个清晰的批处理流水线:

text
清空 output
  → glob content 下所有文件并应用 ignorePatterns
  → 为全部路径计算 slug
  → parseMarkdown
  → 报告 slug 冲突
  → filterContent
  → emitContent
  → public 静态站点

清空输出目录意味着普通全量构建不会保留上一次遗留文件。非 Markdown 文件也会进入 allFiles,便于附件、路径和资源插件使用;只有 Markdown 路径进入 AST 解析。slug 冲突目前是警告而不是立即终止,若两个路径归一化到相同地址,读者应把构建日志中的 collision 当成需要人工处理的内容模型问题。

filterContent 的实现很短,却体现了顺序语义:每个 Filter 都在上一个 Filter 的结果上继续过滤。被前置插件移除的内容不会再被后续插件看到。因此 RemoveDraftsExplicitPublish、加密或自定义可见性规则的相对顺序可能改变结果。

发射阶段并非把所有插件同时启动。emitContent先运行 ComponentResources,让哈希文件名进入上下文;再运行 PageTypeDispatcher,产生虚拟页;最后把普通内容和虚拟内容合并,供 sitemap、RSS、Explorer 索引等其他 Emitter 使用。其余 Emitter 可以并行运行,单个失败会被记录,构建可能以“输出不完整”的警告结束,而不是保证全有或全无。

Markdown 转换:文本、mdast 与 hast

一篇 Markdown 在 createFileParser 中先被读成 vfile,去掉首尾空白,执行 textTransform,再写入相对路径和 slug。随后进入两段 unified processor:

每个文件的异常会由 trace 记录,解析器继续处理其他文件。这种“文件级隔离”适合内容仓库,但也要求使用者检查最终解析数量和错误日志,否则站点可能成功生成却缺少个别页面。

并发策略有一处值得留意的源码与说明漂移。CLI 参数描述和 troubleshooting 文档称默认并发使用 CPU 核心数;固定提交的 parseMarkdown实际按文件数除以 128 估算,并限制为 1 到 4 个线程。超过一个线程时,解析流程先编译 quartz/worker.ts,再以 128 篇为批次分别完成 text→mdast 和 mdast→hast。--concurrency 可以覆盖这一选择,但过高并发同时增加内存与插件初始化成本。

页面类型、虚拟页与 Preact 渲染

Page Type 调度器先按 priority 降序排列页面类型,并收集各页面类型提供的树转换。完整发射分三阶段:

  1. 调用各 Page Type 的 generate,创建虚拟页及其布局。
  2. 将虚拟页加入 allFiles,预渲染 Body 并填充 htmlAst,再渲染普通内容。
  3. 最后输出虚拟页本身。

这个顺序解决了一个不太显眼的问题:Canvas 或 Bases 生成的页面也可能被其他页面嵌入。如果直接逐页写文件,后渲染页面引用先前不存在的虚拟 AST 就会失败。Quartz 选择先建立全局可见的数据,再统一渲染。

普通内容进入页面类型循环后,第一个 match 返回 true 的插件获得该页面。布局解析会合并共享默认值、按 page type 的覆盖和 Page Type 自身声明的 Frame。最终 emitPage把数据交给 renderPage 并写出 .html

renderPage 会深拷贝 hast,避免破坏增量构建缓存中的原树;然后展开嵌入、执行 Page Type 的树转换、解析 Frame,并用 preact-render-to-string 输出完整 HTML。嵌入解析支持整页、标题和块引用。解析器沿祖先链维护 visited,检测到循环时不会无限递归,而是留下可见警告。兄弟节点重复嵌入同一目标仍被允许,因为目标处理完成后会从集合移除。

资源哈希、SPA 与增量重建

资源为什么必须先于页面生成

页面 <head> 需要知道该引用 index.css 还是 index-a3f2c1b.css。因此 ComponentResources 在页面调度前收集所有 Emitter 和组件注册表提供的 CSS、前置脚本和后置脚本。生产模式会压缩并生成八位内容哈希,开发 --serve 模式保留无哈希主文件,并把后置脚本合成单体 IIFE,以缩短重建路径。

生产模式下,组件后置脚本拆成可缓存文件并并行导入,SPA router 被放到最后单独导入。源码注释给出的原因很具体:router 会触发初始 nav,必须等其他组件先注册监听器。若禁用 SPA,框架仍提供 spaNavigate 兼容函数并只派发一次 nav,组件不需要维护两套初始化协议。

当字体源设置为 Google Fonts 且不启用 CDN 缓存时,构建器会联网下载样式和字体文件,还要求配置 baseUrl。这意味着“静态输出”不等于“构建过程必然离线”;是否联网由插件、字体和其他 Emitter 的配置决定。

内容变化走局部链,框架变化走硬重建

内容 watcher 以 content/ 为工作目录,等待写入稳定 250ms,并再做 100ms 调度去抖。变化被合并进 changesSinceLastBuild:新增和修改的 Markdown 才重新解析,删除项从 contentMap 移除,非 Markdown 新文件只更新路径状态。之后仍会重新计算全局 slug、运行所有 Filter,并让 Emitter 选择 partialEmit 或普通 emit。所以“增量”主要节省解析成本,不保证每个派生产物都只处理一个页面。

另一条 watcher 监听 TypeScript、TSX、SCSS、CLI JavaScript、静态资源、package.json 和配置文件。命中后先调用上一次构建返回的清理函数,再让 esbuild 重建,动态导入带新随机 query 的模块。官方架构文档明确指出,这种绕过 import cache 的方式每次源码热重载大约泄漏 350KB 内存;长时间频繁修改配置时,重启开发进程是更稳妥的恢复手段。

失败分支与调试入口

现象源码中的分流位置处理方法与成功信号
启动立即提示 Node 版本过低bootstrap-cli.mjs 的主版本检查升级到 Node 22+,再次执行时不再在 CLI 入口退出
Could not resolve 或插件模块缺失配置装载或 esbuild import先执行 npx quartz plugin install;确认插件出现在 .quartz/plugins/ 或 node_modules
插件依赖缺失、顺序反转或循环validateDependencies按错误提示添加/启用依赖并调整 order;配置装载不再抛 dependency validation error
新克隆中旧锁定插件构建失败插件恢复到 quartz.lock.json 固定提交官方建议 npx quartz plugin install --latest 刷新锁文件;这会改变插件版本,应先评估差异
低内存环境插件安装卡住或 OOM插件并行安装与各自构建使用 --concurrency 12;若固定在同一插件失败,再开启 --verbose 定位
笔记没有出现在站点Filter 或 ignorePatterns检查 draftpublish、目录位置和过滤插件;verbose 日志可显示由哪个 Filter 移除
两篇笔记映射到相同 URLreportSlugCollisions根据 collision 日志修改路径或链接策略,直至构建不再报告冲突
单篇内容解析失败但构建继续createFileParser / createMarkdownParser 的逐文件 catch--verbose 和错误路径修正文档或插件;核对 Parsed 数量是否符合预期
某个 Emitter 失败但仍有输出runEmitteremitErrors查看 Failed to emit from plugin 和“不完整输出”警告,不能只以 public/ 存在判断成功
8080 或 3001 被占用HTTP / WebSocket EADDRINUSE分别用 --port--wsPort 改端口;两个监听都成功后热更新才完整
远程开发能打开页面但不热更新浏览器无法连接本机 WebSocket开放 ws 端口,并通过 --remoteDevHost 指定浏览器可访问地址

值得学习的设计与不足

值得学习

  • AST 分层让文本兼容、Markdown 语义和 HTML 后处理各自拥有明确扩展位置。新插件可以先判断自己改变的是输入语法、语义树还是渲染树,再选择接口。
  • Page Type、Component、Frame 三层拆开了“页面是什么”“页面里有什么”“页面骨架怎样排”。这比为每种页面复制一套完整模板更利于组合,也让插件可以只交付其中一层。
  • 虚拟页先建模、后渲染的顺序,把 Canvas、Bases、标签和目录页纳入同一个可引用内容图,而不是当作构建末尾的特殊文件。
  • 资源 Emitter 明确维护脚本先后关系,生产构建的哈希缓存与开发构建的快速合包分别优化不同目标,没有用同一种输出策略勉强兼顾。
  • 两类 watcher 分开处理内容和框架变化,且用 Mutex 避免构建与 HTTP 读取同时操作输出。这些细节比“支持热更新”这句功能说明更有工程价值。

代价与思考

  • 配置加载不只是读取 YAML,还可能安装 Git 插件、构建代码和安装原生依赖。这套流程把“声明配置”和“改变本地依赖状态”放在相近生命周期内,供应链审查、离线构建和可重复性需要额外纪律。
  • 部分插件装载失败会记录错误后继续,而依赖校验、主构建编译等错误会终止;Emitter 失败又可能留下不完整站点。错误语义并非统一的 fail-fast,CI 应额外检查日志与产物完整性。
  • Page Type 采用优先级加首个匹配,Plugin 又有 order 和依赖顺序。扩展规模变大后,行为可能由多个排序字段共同决定,需要较好的诊断输出和配置约束。
  • 默认解析并发的源码行为与 CLI/文档描述不同。对构建性能做判断时,应以当前提交实现和实际测量为准,不能只引用“使用全部 CPU 核心”。
  • 随机 query 绕过 Node import cache 简洁有效,但官方已承认持续热重载会泄漏内存。这种做法适合开发会话,不应被当成长期驻留进程的模块更新方案。
  • 增量链路会重跑全局 Filter,并允许 Emitter 回退到完整 emit。大型知识库的真实增量成本取决于插件实现,不能由“只解析变化 Markdown”直接推导出线性性能结论。

应用场景与同类方案

Quartz 最适合内容本身具有链接网络、希望兼容 Obsidian 写作习惯、又愿意用 Git 和 Node.js 管理构建的人。个人数字花园、研究笔记、课程知识库和带有双链/图谱的技术文档都是自然场景。若团队只需要传统层级文档,或者不愿承担 Node、插件仓库和静态部署链路,Quartz 的灵活性可能反而增加维护面。

方案主要内容模型扩展与渲染运维与取舍
Quartz v5Markdown、Obsidian 链接、内容关系图AST 插件、Page Type、Component、Frame、Preact SSR静态托管简单;构建端插件与 v5 演进需要维护
HugoMarkdown 与内容分类体系Go 模板、主题、shortcode单文件工具链和构建速度有优势;Obsidian 语义通常需额外适配
MkDocs Material以导航和文档层级为中心的 MarkdownPython 插件、主题扩展适合产品/工程文档;数字花园的双链与自由漫游不是核心模型
Astro / Starlight内容集合与组件化站点Astro/MDX/前端组件自定义站点能力强;要自行组合 Obsidian 兼容、图谱和发布体验
Obsidian PublishObsidian Vault 的托管发布商业托管能力,较少接触构建源码上手和运维负担低;自托管、构建级扩展和成本控制空间较少

选择时可以先问三个问题:是否必须保留 Obsidian 语义;是否希望自己控制静态产物和托管;是否愿意维护插件与版本升级。前两项为“是”且第三项可接受时,Quartz 的架构优势最明显。

构建与部署常见问题

为什么 v4 教程会导致配置失败?

本文分析的是默认分支 v5。v5 把社区插件、YAML 配置、Page Type 和 Frame 作为重要架构层,而 GitHub 最新 Release 页面仍停留在 v4.0.8。迁移时优先使用 v5 仓库内 docs/getting-started/migrating.mdwhats-new.md 和当前配置示例,不要混用历史 quartz.config.ts 片段。

为什么静态站点构建还会访问网络?

插件来源可能是 npm 或 Git;部分插件需要恢复、构建或安装原生依赖;Google Fonts 在特定配置下会由构建器下载。要做离线 CI,需要提前锁定和缓存依赖,选用本地字体,并审计所有 Emitter 的网络行为。

为什么页面能打开,搜索、图谱或 SPA 却异常?

HTML 成功写出只证明页面 Emitter 完成。搜索和 Explorer 依赖内容索引,组件交互依赖后置脚本,SPA 又依赖 router 的最后加载顺序。检查浏览器网络面板中的哈希 CSS/JS、static/contentIndex.json,再查看构建日志中是否有 Emitter failure。

子路径部署为什么容易出现资源 404?

Quartz 会从 baseUrl 计算生产路径,而本地 --serve 会去掉子路径差异。创建配置时应让 baseUrl 与实际域名和路径一致;本地预览子目录可配 --baseDir。部署后同时验证首页、深层页面、404 页面和 SPA 跳转,不能只打开根路径。

如何判断一次源码修改走了哪种重建?

修改 Markdown 时日志应出现 Detected change, rebuilding...,复用 contentMap;修改 TS、SCSS 或配置时会出现 Detected a source code change, doing a hard rebuild...,随后重新编译构建模块。若开发会话在多次硬重建后内存持续增长,直接重启 npx quartz build --serve

总结与源码阅读路径

Quartz v5 可以概括为一台“可编排的内容编译器”:CLI 和配置加载器建立构建环境,Transformer 把 Markdown 变成结构化内容,Filter 决定发布集合,Page Type 将内容路由到页面形态,Component 与 Frame 完成页面组合,Emitter 写出 HTML、索引和资源,浏览器脚本再为静态结果补上 SPA 与交互。

继续阅读可以按目标进入,而无需从仓库第一行顺序翻阅:

  • 想写 Markdown 语法插件:从 processors/parse.ts 和一个 Transformer 的 markdownPlugins/htmlPlugins 开始。
  • 想增加一种页面:从 plugins/pageTypes/dispatcher.ts、Page Type 的 match/generate 和布局覆盖开始。
  • 想增加侧栏组件或全新页面骨架:追 componentRegistryframes/registry.tsrenderPage.tsx
  • 想排查构建缺页:依次看 glob/ignore、解析错误、Filter verbose 日志、Page Type 匹配和 Emitter warning。
  • 想优化开发构建:区分 build.ts 的内容增量链与 handlers.js 的硬重建链,再测量具体插件成本。

参考资料

文章结论绑定 source commit;动态信息以核对日期为准。