<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>0WD0</title>
    <link>https://0wd0.com/</link>
    <description>I build tools for the seams between editors, operating systems, developer workflows, and agent runtimes.</description>
    <language>zh-cn</language>
    <lastBuildDate>Sat, 08 Aug 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://0wd0.com/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>从 Org 到 Web：建站记</title>
      <link>https://0wd0.com/posts/building-this-site/</link>
      <guid isPermaLink="true">https://0wd0.com/posts/building-this-site/</guid>
      <pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate>
      <description>我为什么没有采用 Org → Markdown，以及 ox-edn、Loam 和 SvelteKit 如何把 Org 变成一个网站。</description>
      <category>org</category>
      <category>emacs</category>
      <category>clojure</category>
      <category>sveltekit</category>
      <content:encoded><![CDATA[<article class="org-document" data-page-id="building-this-site:0d3edb2c-00ce-4e89-8f69-bcd6acd34bb3"><p class="org-paragraph">我已经有一个用 Quartz 搭建、可以正常发布的博客，最后还是重新做了这个网站。
</p><p class="org-paragraph">页面生成早已有成熟答案。麻烦来自内容模型：我的写作和知识整理一直发生在 Emacs、Org 和 org-roam 里，旧网站接收的却是转换后的 Markdown。两边使用不同的内容模型，我就得决定哪些 Org 语义值得保留，再在下游逐项补回 ID、链接和页面关系。
</p><p class="org-paragraph">2026 年 5 月，我在知识库里写过一句话：
</p><blockquote><p class="org-paragraph">我们不需要先转换为 Markdown，这对于 Org 文件来说是语义丢失。
</p></blockquote><p class="org-paragraph">这个想法最先落在了 <a href="https://github.com/0WD0/majutsu/tree/main/docs" rel="noopener noreferrer external">Majutsu 的文档站</a>上。那套尚未上线的站点已经使用 <a href="https://github.com/0WD0/ox-edn" rel="noopener noreferrer external">ox-edn</a> 和 <a href="https://github.com/0WD0/loam" rel="noopener noreferrer external">Loam</a> 编译 Org，再通过自定义 Astro content loader 把产物交给 Starlight。它证明了内容层可以脱离 Markdown，也让我看到另一个边界：文档站的侧栏、目录、搜索和前后页有成熟答案，个人网站的页面结构却需要自己决定。
</p><p class="org-paragraph">到 2026 年 8 月 7 日，我已经不需要重新验证内容编译器了。新的问题是，什么样的 Web shell（借用 desktop shell 的说法，指承载已编译内容、负责路由、布局和交互的那一层）适合项目页、长文、threads、backlinks 和逐步增加的交互。于是我保留 ox-edn 和 Loam，把 Starlight 换成了 SvelteKit。
</p><p class="org-paragraph">这篇文章记录的就是这三层边界。
</p><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="why-not-markdown"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#why-not-markdown">我不想把 Org 当成一种比较奇怪的 Markdown</a></h2><p class="org-paragraph">最顺手的方案是把 Org 转成 Markdown，再交给成熟的静态站点生成器。标题、段落、列表和代码块通常都能顺利通过，问题出在我实际依赖的那部分 Org。
</p><p class="org-paragraph">我的 Org 文件里有 ID、<code class="org-verbatim">CUSTOM_ID</code>、property drawer、tags、TODO、source block、target、footnote，以及 id: / fuzzy / file 等不同类型的链接。org-roam 又把稳定的 ID 作为节点身份。<span data-org-link-occurrence id="org-link-1-0-2-3"></span><a href="https://0wd0.com/projects/majutsu/">Majutsu</a> 的手册本身就是 Org；<span data-org-link-occurrence id="org-link-1-0-2-5"></span><a href="https://0wd0.com/projects/touchdeck/">TouchDeck</a> 的笔记会继续连到 niri、fcitx5 和 librime。这些内容共同组成一张持续变化的图。
</p><p class="org-paragraph">如果中间加入 Markdown，我还要定义一套映射规则：
</p><ul><li><p class="org-paragraph">Org <code class="org-verbatim">ID</code> 在 Markdown 里如何表示？
</p></li><li><p class="org-paragraph"><code class="org-verbatim">CUSTOM_ID</code> 和标题 slug 谁决定最终锚点？
</p></li><li><p class="org-paragraph">跨文件 <code class="org-verbatim">id:</code> 链接在哪一层解析？
</p></li><li><p class="org-paragraph">反向链接根据哪一份数据生成？
</p></li><li><p class="org-paragraph">原始文件里的源码区间能否继续追踪？
</p></li><li><p class="org-paragraph">Org parser 的行为变化以后，哪一层负责同步？
</p></li></ul><p class="org-paragraph">ox-hugo 和 Quartz 已经证明这条路线可以工作。放到我的网站里，Markdown 会成为一份需要长期维护的公共中间模型。我更希望下游直接接收 Emacs 解析出来的 Org。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="pipeline-overview"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#pipeline-overview">三层管线各自负责什么</a></h2><p class="org-paragraph">现在的构建过程是：
</p><figure class="org-src-block"><pre><code class="language-text" data-emacs-mode="text-mode" data-highlight-provider="emacs-font-lock">Org
 │  Emacs / org-element + ox-edn
 ▼
Envelope v1 / EDN
 │  Loam
 ▼
Manifest + Search + Graph + HTML + Org source
 │  SvelteKit
 ▼
Static website
</code></pre></figure><p class="org-paragraph">三层各自只处理一类问题：
</p><ul><li><p class="org-paragraph">ox-edn 解析单份 Org，并把结果编码成 EDN；
</p></li><li><p class="org-paragraph">Loam 汇总所有文档，决定页面边界，解析跨文件链接，再生成全站产物；
</p></li><li><p class="org-paragraph">SvelteKit 读取这些产物，负责布局、交互和静态页面。
</p></li></ul><p class="org-paragraph">TypeScript 不解析 Org，Emacs 不决定页面长什么样，Loam 也不维护浏览器组件。多出来的两层就是用来守住这些边界的。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="ox-edn-frontend"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#ox-edn-frontend">ox-edn：让 Emacs 负责解释 Org</a></h2><p class="org-paragraph">ox-edn 的边界很窄：调用 <code class="org-verbatim">org-element</code> 解析单份 Org，再把 AST 和导出信息编码成 EDN。它不决定 URL，也不生成网站。
</p><p class="org-paragraph">核心入口仍然是 Org 自己的 parser：
</p><figure class="org-src-block"><pre><code class="language-emacs-lisp" data-emacs-mode="emacs-lisp-mode" data-highlight-provider="emacs-font-lock">(<span class="ef-elisp-function ef-font-lock-function-name-face" data-emacs-faces="elisp-function font-lock-function-name-face">org-element-parse-buffer</span>)
</code></pre></figure><p class="org-paragraph">我没有在 JavaScript、Rust 或 Clojure 里再实现一个「足够像 Org」的 parser。headline、property、link 和 target 先由正在运行的 Emacs/Org 解释，ox-edn 只负责把结果带出 Emacs。
</p><p class="org-paragraph">生产构建输出带版本的数据结构 Envelope v1，大致长这样：
</p><figure class="org-src-block"><pre><code class="language-clojure" data-emacs-mode="clojure-ts-mode" data-highlight-provider="emacs-font-lock"><span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:</span><span class="ef-font-lock-type-face" data-emacs-faces="font-lock-type-face">ox-edn</span><span class="ef-default" data-emacs-faces="default">/</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">schema-version</span> <span class="ef-font-lock-number-face" data-emacs-faces="font-lock-number-face">1</span>
 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:</span><span class="ef-font-lock-type-face" data-emacs-faces="font-lock-type-face">ox-edn</span><span class="ef-default" data-emacs-faces="default">/</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">exporter</span>
 <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:name</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;ox-edn&quot;</span>
  <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:version</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;...&quot;</span>
  <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:emacs-version</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;...&quot;</span>
  <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:org-version</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;...&quot;</span><span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">}</span>

 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:</span><span class="ef-font-lock-type-face" data-emacs-faces="font-lock-type-face">ox-edn</span><span class="ef-default" data-emacs-faces="default">/</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">source</span>
 <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:path</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;content/posts/building-this-site.org&quot;</span>
  <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:sha256</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;...&quot;</span>
  <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:encoding</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;utf-8&quot;</span><span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">}</span>

 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:</span><span class="ef-font-lock-type-face" data-emacs-faces="font-lock-type-face">ox-edn</span><span class="ef-default" data-emacs-faces="default">/</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">document</span> <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span>...<span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">}</span>
 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:</span><span class="ef-font-lock-type-face" data-emacs-faces="font-lock-type-face">ox-edn</span><span class="ef-default" data-emacs-faces="default">/</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">diagnostics</span> <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">[]}</span>
</code></pre></figure><p class="org-paragraph">这里有三条刻意收紧的约束：
</p><ul><li><p class="org-paragraph">source path 必须是相对仓库根目录的 POSIX path，<code class="org-verbatim">/home/...</code> 之类的机器绝对路径不能进入公开产物；
</p></li><li><p class="org-paragraph">ox-edn 按字节读取源文件，验证内容可以无损往返 UTF-8，并计算 SHA-256，下游因此能确认 AST 对应的是哪一份源文件；
</p></li><li><p class="org-paragraph">生产导出默认使用 strict diagnostic policy，源码区间越界、property 读取失败、未知节点和不可序列化的值都会让导出失败，已有产物不会被半成品覆盖。
</p></li></ul><p class="org-paragraph">链接也按同样的边界处理。对于 Org 能在当前文档中解析的 ID、custom ID、fuzzy 和 radio link，ox-edn 会附上目标 ID 与源码区间等提示。跨文档解析留给 Loam，因为它需要看到完整的内容集合。
</p><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="htmlize-comparison"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#htmlize-comparison">代码高亮沿用 Emacs，HTML 留给 Loam</a></h3><p class="org-paragraph">Org 的 HTML exporter 已经可以借助 htmlize 处理代码高亮。<code class="org-verbatim">ox-html</code> 进入对应语言的 major mode，让 font-lock 产生 face，再由 htmlize 输出 inline style 或 CSS class。如果目标只是一份 Org 对应一个 HTML 页面，我会直接使用 <code class="org-verbatim">ox-html + htmlize</code>。
</p><p class="org-paragraph">这个网站需要的边界更早一些。ox-edn 记录 face 及其字符区间，把 <code class="org-verbatim">span</code> 留给后面的渲染阶段：
</p><figure class="org-src-block"><pre><code class="language-clojure" data-emacs-mode="clojure-ts-mode" data-highlight-provider="emacs-font-lock"><span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:provider</span> <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:emacs-font-lock</span>
 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:mode</span> <span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;svelte-ts-mode&quot;</span>
 <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:runs</span> <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">[{</span><span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:start</span> <span class="ef-font-lock-number-face" data-emacs-faces="font-lock-number-face">1</span>
         <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:end</span> <span class="ef-font-lock-number-face" data-emacs-faces="font-lock-number-face">7</span>
         <span class="ef-clojure-ts-keyword-face" data-emacs-faces="clojure-ts-keyword-face">:faces</span> <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">[</span><span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;font-lock-keyword-face&quot;</span><span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">]}]}</span>
</code></pre></figure><p class="org-paragraph">Emacs 在这里回答「哪些字符带有哪些 face」。Loam 稍后把这些区间转成经过约束的 HTML class，网站样式表再决定颜色。当前 Emacs 主题的 RGB 值不会被写进内容。
</p><p class="org-paragraph">这条路径保留了 htmlize 最吸引我的部分：代码仍由实际使用的 major mode 处理。代价也很明确，结果依赖本机安装的 major mode 和个人配置。固定版本的 Shiki 或 Tree-sitter grammar 更适合追求环境无关的通用 CI；我的目标则是让网页尽量复用日常编辑时的语言配置。
</p></section></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="loam-compiler"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#loam-compiler">Loam：从 Org AST 得到全站页面</a></h2><p class="org-paragraph">ox-edn 回答一份 Org 写了什么，Loam 处理这些 Org 在同一个网站里的关系。
</p><p class="org-paragraph">严格编译管线大致是：
</p><figure class="org-src-block"><pre><code class="language-text" data-emacs-mode="text-mode" data-highlight-provider="emacs-font-lock">load
  → validate
  → normalize
  → partition
  → index
  → resolve
  → render
  → emit
</code></pre></figure><p class="org-paragraph">其中最重要的一步是 partition。一棵 Org AST 不必对应一个网页：页面可以来自完整的 Org 文件，也可以来自某个文件中的一个 subtree，这两种形态也正好对应 org-roam 的文件节点和标题节点。
</p><p class="org-paragraph">personal profile 目前采用两条页面归属规则：
</p><ul><li><p class="org-paragraph">文档根节点同时声明 <code class="org-verbatim">ID</code> 和 <code class="org-verbatim">EXPORT_FILE_NAME</code> 时，整个 Org 文件构成一个页面；
</p></li><li><p class="org-paragraph">文档根节点没有声明这组属性时，带有页面身份和发布路径的一级 headline 各自构成一个页面，其 subtree 是该页面拥有的源码。
</p></li></ul><p class="org-paragraph">页面归属只能由一个 <code class="org-verbatim">:partitioner</code> 提供。这样，哪个 subtree 属于哪个 URL 不会受 extension 加载顺序影响。已有的 <code class="org-verbatim">:page-partitioners</code> 仍可在分页后追加转换，但不能重新定义页面身份。
</p><p class="org-paragraph">文档页的 metadata 继续写在 Org 里：
</p><pre class="org-example">:PROPERTIES:
:ID: 0d3edb2c-00ce-4e89-8f69-bcd6acd34bb3
:EXPORT_FILE_NAME: posts/building-this-site
:LOAM_KIND: post
:LOAM_STATUS: stable
:LOAM_PUBLISHED_AT: 2026-08-08
:END:
#+FILETAGS: :org:emacs:sveltekit:
</pre><p class="org-paragraph">Loam 随后为页面、Org ID、<code class="org-verbatim">CUSTOM_ID</code>、headline、target 和 anchor 建立全局索引，再解析跨文件链接。正向链接、反向链接和 graph edge 都来自同一次解析，不需要由 Web 层重新推断。
</p><p class="org-paragraph">最终构建产物包括：
</p><figure class="org-src-block"><pre><code class="language-text" data-emacs-mode="text-mode" data-highlight-provider="emacs-font-lock">manifest.json
build-report.json
search-index.json
graph.json
pages/*.html
source/**/*.org
</code></pre></figure><p class="org-paragraph"><code class="org-verbatim">manifest.json</code> 记录 route、title、description、tags、status、源码区间、headings、backlinks 和 outgoing links；<code class="org-verbatim">search-index.json</code> 是搜索需要的精简数据；<code class="org-verbatim">graph.json</code> 只保留节点和边；<code class="org-verbatim">source/**/*.org</code> 保存每个公开页面实际拥有的 Org source。正文在这一层已经变成确定的 HTML fragment。
</p><p class="org-paragraph">emit 之前，Loam 会递归检查公开 manifest、report 和 HTML fragment。发现机器绝对路径就拒绝发布。strict renderer 遇到未知 Org node 也不会只渲染 children 后继续构建，任意 HTML export block 同样不能直接穿透到页面。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="sveltekit-shell"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#sveltekit-shell">SvelteKit 只消费构建产物</a></h2><p class="org-paragraph">Loam 生成完整的一代产物后，SvelteKit 只读取 <code class="org-verbatim">.cache/loam</code>。TypeScript 不读 EDN，也不重新解释 Org AST。catch-all route 从 manifest 枚举路径，构建时加载对应的 HTML fragment，再由 <code class="org-verbatim">adapter-static</code> prerender 成普通静态文件。
</p><p class="org-paragraph">页面组件的核心接近这样：
</p><figure class="org-src-block"><pre><code class="language-svelte" data-emacs-mode="svelte-ts-mode" data-highlight-provider="emacs-font-lock"><span class="ef-font-lock-delimiter-face" data-emacs-faces="font-lock-delimiter-face">&lt;</span><span class="ef-font-lock-function-name-face" data-emacs-faces="font-lock-function-name-face">article</span> <span class="ef-font-lock-property-name-face" data-emacs-faces="font-lock-property-name-face">class</span><span class="ef-font-lock-operator-face" data-emacs-faces="font-lock-operator-face">=</span><span class="ef-font-lock-string-face" data-emacs-faces="font-lock-string-face">&quot;org-content&quot;</span><span class="ef-font-lock-delimiter-face" data-emacs-faces="font-lock-delimiter-face">&gt;</span>
  <span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">{</span><span class="ef-font-lock-misc-punctuation-face" data-emacs-faces="font-lock-misc-punctuation-face">@</span><span class="ef-font-lock-keyword-face" data-emacs-faces="font-lock-keyword-face">html</span> data.html<span class="ef-font-lock-bracket-face" data-emacs-faces="font-lock-bracket-face">}</span>
<span class="ef-font-lock-delimiter-face" data-emacs-faces="font-lock-delimiter-face">&lt;/</span><span class="ef-font-lock-function-name-face" data-emacs-faces="font-lock-function-name-face">article</span><span class="ef-font-lock-delimiter-face" data-emacs-faces="font-lock-delimiter-face">&gt;</span>
</code></pre></figure><p class="org-paragraph"><code class="org-verbatim">{@html}</code> 的信任边界在构建阶段。HTML 来自受控 Org source，并经过 Loam 的严格 renderer。外部内容一旦进入这条管线，就必须重新设计这层安全边界。
</p><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="why-sveltekit"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#why-sveltekit">为什么是 SvelteKit</a></h3><p class="org-paragraph">到了这一层，输入已经是 HTML 和 JSON，Web shell 不会再影响 Org 的内容模型。Astro、Nuxt、Next.js，甚至一组手写模板都能接在 Loam 后面。
</p><p class="org-paragraph">我已经在 Majutsu 文档站里用过 Astro + Starlight。那是一个合适的组合：Starlight 直接提供文档侧栏、页面目录、搜索、前后页和主题切换，我的工作集中在接入 Loam 的 loader 和少数组件上。
</p><p class="org-paragraph">个人网站的信息架构更自由。首页、项目页、thread、文章两侧的关系栏、revision history 和内部链接预览都要按自己的内容模型设计。继续使用 Starlight，接下来的工作会变成逐项替换它的文档外壳；预设更少的 SvelteKit 做起来更省力。
</p><p class="org-paragraph">部署方式也很明确：最终产物必须是纯静态文件。<code class="org-verbatim">adapter-static</code> 保留了 SvelteKit 的路由和数据加载方式，生产服务器却只需要提供 HTML、CSS、JavaScript 和 JSON，不必运行 Node。
</p><p class="org-paragraph">在组件层面，Svelte、Vue 和 React 都能完成这些交互，最后的选择取决于交互的形状。这个网站没有组件库，也没有复杂的全局状态；目前的交互只有搜索面板、链接预览、修订记录和公式复制，状态大多留在单个组件里。Svelte 的单文件组件和局部 CSS 适合这种自定义页面，<code class="org-verbatim">$state</code> / <code class="org-verbatim">$derived</code> 又刚好对应局部状态和派生数据。
</p><p class="org-paragraph">Vue 是最接近的组件层备选。Vue SFC、Composition API 和 Nuxt 的静态生成同样能完成这些工作。我没有做一轮框架 benchmark；选择依据是自己更喜欢 Svelte 在小型交互上的写法，也打算让将来的 Org component islands 使用 Svelte。这个网站不依赖组件库，React 庞大的生态在这里帮不上太多忙。
</p><p class="org-paragraph">还有一个原因来自前两层的设计：内容已经由 Loam 编译完成，我不需要前端框架再解析 Markdown、管理 front matter 或建立一套 content collection。SvelteKit 只接收构建产物，处理路由、组件和静态导出。
</p><p class="org-paragraph">SvelteKit 是一个有前情的务实选择，替换成本也被限制在 Web 层。以后即使换掉它，Org 的页面身份、链接和修订模型也不需要重做。
</p></section><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="sveltekit-build"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#sveltekit-build">构建复用我日常使用的 Doom Emacs</a></h3><p class="org-paragraph">构建脚本递归查找 <code class="org-verbatim">content/</code> 下的 <code class="org-verbatim">.org</code> 文件，再通过 <code class="org-verbatim">emacsclient</code> 把整批导出交给我日常使用的 Doom Emacs。Org parser、个人配置和语言到 major mode 的映射因此来自同一个 Emacs 环境。如果默认 server 不在运行，脚本会启动一个正常加载 Doom 配置的临时 daemon。
</p><p class="org-paragraph">所有文件成为 Envelope 后，构建脚本一次性交给 Loam：
</p><figure class="org-src-block"><pre><code class="language-shell" data-emacs-mode="sh-mode" data-highlight-provider="emacs-font-lock">bb bundle --profile personal ...
</code></pre></figure><p class="org-paragraph">搜索索引不会进入每个页面的 prerender payload。SearchPalette 第一次打开时才获取 <code class="org-verbatim">/search-index.json</code>；文章页也只携带当前页面需要展示的 outgoing target 摘要。新增文章不会仅仅因为搜索索引变化，就改写每一篇旧文章的 HTML。
</p></section></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="org-publishing-landscape"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#org-publishing-landscape">把这套架构放回 Org publishing 生态里</a></h2><p class="org-paragraph">比较 ox-edn 时，需要把视角从代码高亮扩展到完整的 Org publishing：谁解释 Org，什么构成公开页面，全站关系又在哪一层建立。
</p><p class="org-paragraph"><a href="https://orgmode.org/worg/org-blog-wiki.html" rel="noopener noreferrer external">Worg 的 Blogs and Wikis with Org</a> 收录了不少不同年代的方案。我用它观察各个项目如何安排 Org parser、页面边界和 Web 层，功能数量只作参考。
</p><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="emacs-u76f4-u63a5-u751f-u6210-html"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#emacs-u76f4-u63a5-u751f-u6210-html">Emacs 直接生成 HTML</a></h3><p class="org-paragraph"><code class="org-verbatim">org-publish + ox-html</code> 是最完整的官方 baseline。<a href="https://orgmode.org/manual/Publishing.html" rel="noopener noreferrer external">Org manual 的 Publishing 章节</a>把它定义为一套面向互链 Org 文件的发布系统：project 可以组合多个 component，HTML 之外的附件可以一同复制，file link 中的 heading、custom ID 和 target 也会转换成对应的网页锚点。它还内置 sitemap、跨文件 index 和基于 timestamp 的增量发布。公开内容天然是一份 Org 文件对应一份网页时，这套能力已经很完整。
</p><p class="org-paragraph">它的核心抽象始终是「选择一组文件，对每个文件调用 publishing function」。默认页面边界由文件决定。我的内容模型还需要另一层：完整文件可以成为页面，文件中的 subtree 也可以拥有独立 ID、URL 和精确 source。<code class="org-verbatim">org-publish</code> 支持写 Elisp 改造 publishing function，但这套 logical page 模型需要自己实现。
</p><p class="org-paragraph">全站关系也会改变增量构建的含义。假设 B 新增一条指向 A 的链接，B 的源文件发生了变化，A 的 backlink 面板也要随之更新。基于源文件 timestamp 的发布模型只会自然地选中 B；目标页 A 的反向依赖需要额外追踪。Org manual 也提醒，<code class="org-verbatim">SETUPFILE</code> 和 <code class="org-verbatim">INCLUDE</code> 等外部依赖变化时可能需要强制 republish。Loam 会先建立完整索引，再把 backlinks、outgoing links 和前后页等关系计入每个页面的 <code class="org-verbatim">pageBuildDigest</code>。
</p><p class="org-paragraph">HTML 作为主产物也限制了下游能看到的信息。搜索、graph、thread 和 revision history 需要结构化的页面 metadata 与链接边，单独遍历最终 HTML 很难还原 Org ID、源码区间和链接类型。<code class="org-verbatim">org-publish</code> 提供 preparation function、completion function 和自定义 exporter，这些扩展点都能承载上述逻辑。代价是 partition、全局索引、依赖失效和多种构建产物会逐渐堆进同一套 Elisp 发布配置。
</p><p class="org-paragraph"><a href="https://github.com/bastibe/org-static-blog" rel="noopener noreferrer external">org-static-blog</a> 和 <a href="https://github.com/emacs-love/weblorg" rel="noopener noreferrer external">weblorg</a> 把文章列表、归档、Feed、tag pages、模板和主题等常见站点需求继续补齐。如果只写传统博客，这条管线比 ox-edn → Loam → SvelteKit 短得多。我的网站则把 corpus compiler 单独放进 Loam，让 ox-html 和 Web shell 都保持较窄的职责。
</p></section><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="u4ece-u73b0-u6709-org-u5de5-u4f5c-u6d41-u4e2d-u9009-u62e9-u9875-u9762"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#u4ece-u73b0-u6709-org-u5de5-u4f5c-u6d41-u4e2d-u9009-u62e9-u9875-u9762">从现有 Org 工作流中选择页面</a></h3><p class="org-paragraph"><a href="https://github.com/novoid/lazyblorg" rel="noopener noreferrer external">lazyblorg</a> 是与我的需求最接近的现成方案之一。文章可以位于普通 Org 文件的不同位置；除了与日期绑定的 temporal pages，它还提供持续更新的 persistent pages，以及 backlinks、TagTrees、Feed 和原始 Org source。
</p><p class="org-paragraph">它使用自己的 Python parser，并在部分语法上回退到 Pandoc，也有一套围绕 <code class="org-verbatim">:blog:</code> tag、ID 和 DONE/LOGBOOK 的发布约定。这些约定与 Karl Voit 的工作流高度契合。我的工作流则要求网站复用正在运行的 Emacs/Org，包括自己的 Org fork 和 Doom 中的语言映射。
</p></section><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="org-u8f6c-u6210-markdown"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#org-u8f6c-u6210-markdown">Org 转成 Markdown</a></h3><p class="org-paragraph">这是我实际用过最久的一条路线。<a href="https://ox-hugo.scripter.co/" rel="noopener noreferrer external">ox-hugo</a> 支持 one-post-per-file 和 one-post-per-subtree，也会利用 property、tag inheritance 和 TODO state 生成 Hugo front matter。它对 Org 的支持远比「转一下 Markdown」这句话听起来完整。
</p><p class="org-paragraph">进入 Hugo 或 Quartz 后，系统里仍然会同时存在 Org 和 Hugo-flavored Markdown 两套内容模型：
</p><figure class="org-src-block"><pre><code class="language-text" data-emacs-mode="text-mode" data-highlight-provider="emacs-font-lock">Org / org-roam
      ↓ ox-hugo
Hugo-flavored Markdown + front matter
      ↓ Hugo / Quartz
Web content model
</code></pre></figure><p class="org-paragraph">我遇到的麻烦主要发生在写作阶段。为了让服务于 org-roam、agenda 和项目管理的 Org 同时满足 Hugo 的页面模型，我会提前考虑 heading 能否继续嵌套、subtree 是否必须成为 export leaf，以及某个 property 究竟服务于哪一层。
</p><p class="org-paragraph">Loam 的 partitioner 仍然需要决定页面边界，但它直接在原始 Org AST 上完成这一步，不再生成另一份过渡 Org 或 Markdown。
</p></section><section class="org-section" data-org-type="headline"><h3 class="org-heading" id="u72ec-u7acb-org-parser-u548c-graph-first-u5de5-u5177"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#u72ec-u7acb-org-parser-u548c-graph-first-u5de5-u5177">独立 Org parser 和 graph-first 工具</a></h3><p class="org-paragraph"><a href="https://github.com/teesloane/firn" rel="noopener noreferrer external">Firn</a> 使用 Rust 和 Orgize 从 Org 文件生成静态站点；<a href="https://orga.js.org/guides/orga-build/" rel="noopener noreferrer external">Orga / orga-build</a> 把 Org AST 带进 JavaScript、Unified 和 JSX 生态。它们不要求构建机启动 Emacs，更容易进入普通 CI。相应的取舍是，编辑器和构建系统分别由两个 parser 解释 Org。
</p><p class="org-paragraph">另一类工具直接以 graph 为主界面。<a href="https://github.com/ikoamu/publish-org-roam-ui" rel="noopener noreferrer external">publish-org-roam-ui</a> 可以从 org-roam files + database 生成静态 digital garden；<a href="https://github.com/tani/org-roam-ui-lite" rel="noopener noreferrer external">org-roam-ui-lite</a> 也把静态导出作为主要能力。如果目标是把 org-roam graph 放到网上浏览，它们比重做一套个人网站直接得多。我的网站以长文和项目页为主，graph 处在辅助位置。
</p><p class="org-paragraph">把这些方案放在一起，我会这样比较：
</p><div class="org-table-scroll" style="overflow-x:auto;max-width:100%;margin:1em 0;-webkit-overflow-scrolling:touch"><table role="table" cellspacing="0" cellpadding="0" style="border-collapse:collapse;width:max-content;min-width:32rem;max-width:none;font-size:.95em"><thead><tr><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">方案</th><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">Org 语义由谁解释</th><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">公开页面来自哪里</th><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">Org 后的主要表示</th><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">全站能力</th><th scope="col" nowrap style="padding:.45em .65em;border:1px solid #bbb;text-align:left;vertical-align:top;font-weight:600;white-space:nowrap">对我的取舍</th></tr></thead><tbody><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal"><code class="org-verbatim">org-publish + ox-html</code></td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">GNU Emacs Org</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Org 文件</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">HTML</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">sitemap / index 内置，其余可由 Elisp 扩展</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">subtree 页面与关系依赖需要自己实现</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal"><code class="org-verbatim">org-static-blog / weblorg</code></td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">GNU Emacs Org</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Org 文章文件</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">HTML</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">以 blog / site 为中心</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">管线简单，但页面模型以文章为主</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">lazyblorg</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Python parser + Pandoc fallback</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Org 文件中选出的 heading</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">HTML</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">persistent page、backlink、TagTrees、source</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">使用自己的 parser 和发布约定</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal"><code class="org-verbatim">ox-hugo → Quartz</code></td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Emacs export + 下游 Markdown parser</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Org 文件或 <code class="org-verbatim">EXPORT_FILE_NAME</code> subtree</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Hugo Markdown + front matter</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Quartz 提供 backlink、tag 和 graph</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">同时维护两层内容模型</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Firn / Orga</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">Orgize / Orga parser</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">目录中的 Org 文件</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">AST / HTML</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">适合 wiki 和 Web 集成</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">编辑器与构建 parser 可能分叉</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">org-roam-ui publishing</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">org-roam DB + Org files</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">org-roam node</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">graph / search data</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">graph 和 backlink 是主模型</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">主要优化 graph browsing</td></tr><tr><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">ox-edn + Loam</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">我正在使用的 Emacs/Org</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">单个 Org 文件，或某个 Org 文件中的一个 subtree</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">EDN</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">ID、backlink、thread、graph、source、revision</td><td style="padding:.45em .65em;border:1px solid #ccc;text-align:left;vertical-align:top;min-width:6em;word-break:normal;overflow-wrap:normal">自己维护 compiler，复杂度最高</td></tr></tbody></table></div><p class="org-paragraph">这套架构用最高的维护成本换来了我最在意的 authoring model：日常的 Emacs / org-roam 工作流先存在，网站从中选出明确的公开内容，再沿用同一套 Org 语义生成 Web 页面。只写传统博客时，我会直接选择更短的管线。
</p></section></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="personal-site"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#personal-site">发布边界：知识库与公开网站分开管理</a></h2><p class="org-paragraph">我的日常工作本来就在代码和 Org 之间来回。Majutsu 有 Org 手册和 Elisp 源码，TouchDeck 有 Rust/Wayland 实现和与 niri、fcitx5、librime 相连的设计笔记。很多文章在动笔之前，已经以代码、设计文档或 org-roam note 的形式存在。
</p><p class="org-paragraph">网站不应该迫使我为了发布再维护一套内容。但这也不意味着整个 <code class="org-verbatim">~/org</code> 都应该公开。
</p><p class="org-paragraph">目前 <code class="org-verbatim">personal-site/content/</code> 是明确的发布边界：知识库负责思考和积累，content 负责选择哪些内容成为公开作品。以后可以加入更方便的选取、引用和 transclusion，但自动化不能取消这条安全边界。公开页面中隐藏的 drawer 和 comment 仍会保留在 <code class="org-verbatim">/source/...org</code> 里，因此放进 content 的文件本身就必须适合公开。
</p><p class="org-paragraph">我更愿意把这个网站叫作 personal technical workbench。项目、长文、持续更新的 notes、backlinks、threads 和搜索，是同一批知识在不同阶段的视图。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="publishing-revisions"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#publishing-revisions">页面身份、内容修订和构建结果分开计算</a></h2><p class="org-paragraph">文章发布以后，正文、链接关系和网站外壳会以不同速度变化。只用一个文件哈希无法同时表达这些变化。
</p><p class="org-paragraph"><code class="org-verbatim">:ID:</code> 的 UUID 是页面的稳定身份，<code class="org-verbatim">:EXPORT_FILE_NAME:</code> 决定稳定 URL。<code class="org-verbatim">:LOAM_PUBLISHED_AT:</code> 记录第一次公开的日期，修改正文时保持不变；只有读者值得注意的实质修改才设置 <code class="org-verbatim">:LOAM_UPDATED_AT:</code>。错别字、CSS 和部署脚本的改动不必把文章标记为更新。
</p><p class="org-paragraph">构建侧使用三个 digest：
</p><dl><dt><code class="org-verbatim">sourceRevision</code></dt><dd><p class="org-paragraph">公开 Org source 的 SHA-256。一文件一文章时，它覆盖完整文件；collection page 则只覆盖该页面拥有的 subtree。
</p></dd><dt><code class="org-verbatim">contentRevision</code></dt><dd><p class="org-paragraph">标题、公开 metadata 和渲染正文的内容版本。comment 等不进入公开内容的改动不会产生新的 editorial revision。
</p></dd><dt><code class="org-verbatim">pageBuildDigest</code></dt><dd><p class="org-paragraph">页面内容及其关系依赖的构建版本。正文没变，但另一篇文章新增了指向它的 backlink，这一项仍然会变化。
</p></dd></dl><p class="org-paragraph">SvelteKit 默认使用构建时间作为 app version。相同输入连续构建两次时，runtime chunk 名称和页面里的 bootstrap script 也可能变化。我改用 <code class="org-verbatim">src/</code> 和 Svelte 配置计算稳定的 site-shell revision，并把搜索索引改成按需加载。在相同构建环境中，连续两次相同构建现在会生成字节一致的文章 HTML。
</p><p class="org-paragraph">每次 release 仍然是一代完整、不可变、可以原子回滚的静态文件。部署时用：
</p><figure class="org-src-block"><pre><code class="language-shell" data-emacs-mode="sh-mode" data-highlight-provider="emacs-font-lock">rsync --checksum --link-dest=&lt;previous-release&gt; build/ &lt;new-release&gt;/
</code></pre></figure><p class="org-paragraph">没有变化的文件直接 hardlink 到上一代，只有变化的文件需要传输。删除旧 release 时，只要新 release 仍引用相同 inode，文件数据就不会消失。完整 generation 与跨代复用可以同时成立。
</p><p class="org-paragraph">公开的页面更新历史只跟踪 editorial revision。一个 jj change 可能同时修改网站代码和几篇文章，也可能只做重构。每次正式部署会合并上一代的 <code class="org-verbatim">revision-history.json</code> 与当前 manifest；同一页面 UUID 的 <code class="org-verbatim">contentRevision</code> 变化时，才追加一条公开修订记录。
</p><p class="org-paragraph">每个页面还有对应的 <code class="org-verbatim">/source/...org</code>。文章页公开完整原始文件，collection page 公开原文件中的精确 subtree，不把 AST 重新导出成「看起来像 Org」的文本。
</p><p class="org-paragraph">Tag 则被投影成 thread。<code class="org-verbatim">/threads/emacs/</code>、<code class="org-verbatim">/threads/wayland/</code> 按 writing、project 和 field note 分组，related threads 来自 tag co-occurrence；搜索中输入 <code class="org-verbatim">#emacs</code> 时也优先匹配 tag。这些页面仍然来自同一份 Org corpus，不需要另一套内容数据库。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="syntax-showcase-link"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#syntax-showcase-link">语法展示留在单独的回归测试里</a></h2><p class="org-paragraph">完整的语法展示有两百多行，覆盖链接、列表、special block、LaTeX、source block、表格、脚注和 drawer。全部塞进正文会把建站文章变成 renderer fixture。
</p><p class="org-paragraph">这些内容现在放在单独的 <span data-org-link-occurrence id="org-link-9-0-2-1"></span><a href="https://0wd0.com/notes/org-syntax-showcase/">Org 语法陈列室</a>。那个页面通过正常内容管线发布，同时充当真实的回归测试；这里则只保留与架构有关的例子。
</p></section><section class="org-section" data-org-type="headline"><h2 class="org-heading" id="self-hosting-post"><a class="org-heading-anchor" href="https://0wd0.com/posts/building-this-site/#self-hosting-post">这篇文章就是一次端到端构建</a></h2><p class="org-paragraph">这篇文章的源文件是：
</p><pre class="org-example">content/posts/building-this-site.org
</pre><p class="org-paragraph">网页侧栏里的 <code class="org-verbatim">view Org</code> 指向本次构建实际发布的 <code class="org-verbatim">/source/posts/building-this-site.org</code>。这份源文件先由 Emacs 解析，经 ox-edn 编码成 EDN；Loam 为它分配 <code class="org-verbatim">/posts/building-this-site/</code>，解析链接并生成正文；SvelteKit 最后把页面 prerender 成静态 HTML。
</p><p class="org-paragraph">本文直接使用自己的源文件走完整条管线，也成了这个网站的第一份端到端构建测试。
</p></section></article>]]></content:encoded>
    </item>
  </channel>
</rss>
