跳转到主要内容

Oink 0.8.0:整个栏目一次取走,侧栏直接当数据读,以及谁链到了这一页

Oink 0.8.0 为「以程序身份来读」的访客加了两种可选输出格式:把整个栏目装进一个文件的 全文包,以及以 JSON 发布的导航树;再加上静态反向链接,在右栏列出链接到本页的页面。 三者都默认关闭,不点名就不会出现。

Oink 0.8.0 不改动任何组件 API,也不需要修改内容。三项新增里有两项服务于以程序身份来读的访客。 每一页本来就会多产出一份 .md,它服务的是已经知道自己要哪一页的 agent; 而想读完整份手册的 agent,仍然只能一页一页地爬,边爬边发现链接。 这两种新输出格式回答的是另一半问题:把整个栏目给我,以及在我开始抓之前,先告诉我这个站点里有什么。 第三项是给作为人的读者的:页面右栏现在可以列出有哪些页面链接到它。

概览

  • LLMSFULL 为每个顶层栏目产出一份 llms-full.txt:栏目下的每一页,按侧栏阅读顺序,装在一个文件里。
  • NAVJSON 为每种语言产出一份 navigation.json:侧栏那棵树,以数据形式发布,并由 JSON Schema 定版。
  • params.ui.backlinks 在右栏列出链接到本页的页面,索引在构建时从你 Markdown 里本来就有的链接派生。
  • 三者都是可选的,主题绝不会替你打开。三个都不点名的站点,构建结果与此前逐字节一致。
  • llms.txt 会列出你开启的那些文件,发现入口仍留在 agent 本来就会抓的那个文件里。
  • data/docs_nav.json 里没有 children 键的节点不再中断构建。

全文包:整个栏目一次取走

LLMSFULL 把整个栏目收进一个文件:栏目根目录下的 llms-full.txt, 按侧栏与翻页器呈现的顺序把栏目下每一页依次拼接,每页之前有一行分隔符标出它的来源地址。 对 agent 来说,/docs/llms-full.txt 是一次抓取,而原来的做法是每页一次抓取外加一张要跟着走的链接图; 而且结果是有序的,于是这个栏目读起来像一份手册,而不是一堆页面。

开关在栏目自己的 front matter 上,主题不会把它加进站点的输出集合:

content/docs/_index.zh.md
---
title: 文档
outputs: [HTML, print, RSS, markdown, LLMSFULL]
---

front matter 里的 outputs 是对站点级列表的整体替换,因此要把这个栏目原本就有的格式一并写回。 它按语言生效,所以中文全文包需要在 _index.zh.md 里同样写一遍。

每一页贡献进来的,就是它自己 .md 里那份语义 Markdown,而不是第二次渲染的结果。 每页 Markdown 正文已经挪进两种输出共用的同一个 partial,因此全文包中的一段与该页的 .md 逐字节相同, 两者不可能各走各的。顺序同样来自侧栏读的那份权威:docsbook 栏目声明了 data/docs_nav.json 显式树时以显式树为准,其余按内容树的 weight。不在侧栏里的页面,也不会进全文包。

全文包属于顶层栏目,没有整站版本:想要全部内容的 agent,一个栏目读一份。 写在更深一层的栏目上会告警并且什么都不产出,于是 hugo server 照常能用, 而加了 --panicOnWarning 的发布构建会停在这里。

本站的文档栏目已经开启,https://oink.pgsty.com/zh/docs/llms-full.txt 一次取走全部中文文档。文件的确切形状等细节见全文包

侧栏是站点的目录,读得懂它的 agent 可以在正文上花掉第一次抓取之前,先规划好路线。 NAVJSON 把它作为数据发布出来:每种语言一份 navigation.json,放在语言根目录下。 由站点在首页打开:

hugo.yml
outputs:
  home: [HTML, LLMS, NAVJSON]

这棵树不是对站点结构的第二次描述。它序列化的是侧栏与翻页器本来就在读的那份权威, 走的也是同一个 partial:声明了 data/docs_nav.json 显式树的地方以显式树为准,其余按内容树的 weight。 有一项检查断言 docs 子树展平后恰好等于全文包产出的页面序列——两条模板路径,一份权威。

每个节点带 id(去掉语言前缀的路径,因此同一页在每种语言里 id 相同)、绝对地址 url、 页面确实产出 .md 时的 markdown 地址、titledescriptionkind,以及有序的 children。 其中两条值得当作承诺而不是实现细节来读:

  • 数组顺序就是契约。顺序已经算好了,weight 不会被序列化——消费方再排一次,只会与它来源的侧栏对不上。
  • 格式带版本。schemaVersion1,契约随主题仓库发布,见 schema/nav.v1.schema.json。要消费这个文件,就拿它做校验。

本站的 https://oink.pgsty.com/zh/navigation.json 就是实例。占位条目与完整键表等细节见导航 JSON

从搜索落到一个页面的读者,只能看到这一页指向哪里,看不到它自己处在什么位置。 反向链接补上的就是这另一半:右栏目录下方多出一个「反链」组,列出有哪些页面链接到它, 默认展开,超过八条折进「再显示 N 条」。一个键就能打开:

hugo.yml
params:
  ui:
    backlinks: true

单页用 front matter 键 backlinks 覆盖,分区用 cascade 覆盖它下面的所有页面。

索引在构建时从你本来就写好的东西里派生:页面源码里的普通 Markdown 链接,以及 ref / relref。 没有 [[wikilink]] 这类新语法要采纳,没有内容要迁移,也不需要 JavaScript—— 链接就在 HTML 里,也在这一页的 Markdown 输出里,关掉脚本的读者一样看得到。 扫描前先剥掉代码围栏与行内代码;指向同一目标的多个链接合并成一条; 自链接、外链与同页锚点都不计入;每种语言各有一张图。 顺序是稳定页面路径,因此同样的内容永远构建出同样的列表;没有页面链进来时,整个区块不出现。

有一处需要说清楚的遗漏:读源码看不见藏在自定义 shortcode 参数里或原始 <a href> 里的 URL, 解析不出来的目标也会被静默丢弃。这是导航,不是链接检查——查断链仍然要用链接检查器。

本站全站开启:看任何一篇文档的右栏就能看到,被引用最多的配置总览列出了四十多个入链。细节见反向链接

发现入口仍在 llms.txt

两个文件都不是某个页面的替代表示,因此都不会出现在 <head> 里,也不会有对应的页面动作。 取而代之的是 llms.txt——agent 本来就会先抓的那个文件——多出一段 ## Full-text bundles 列出本语言的全部全文包,并在站点索引里列出本语言的 navigation.json。 两处条目都只在站点确实发布了对应文件时才出现:主题绝不指向自己没有产出的东西。

没有 children 的导航节点不再中断构建

data/docs_nav.json 里没有 children 键的节点,会让构建以侧栏遍历器内部抛出的一个反射错误告终。 遍历器假定每个节点都带这个键——这对生成的 JSON 成立,对人手写的 JSON 不成立, 因为手写时叶子节点很自然地就写成一个没有 children 的节点。 现在作者写的数据会降级而不是报错:没有子节点的节点,就按它本来的样子渲染成叶子。

升级

hugo mod get github.com/pgsty/[email protected]
hugo mod tidy

不点名就什么都不会变。组件 API 没有改动,也不需要修改内容——两种输出格式在 outputs 里声明, 反向链接是 params.ui 下的一个布尔;三个都不点名的站点,发布出来的东西和 0.7.1 一样。 两种输出格式以及它们产出的东西长什么样,都在 Agent 支持; 反向链接的开关见导航与菜单

完整清单见 CHANGELOG.md