跳转到主要内容

Agent 支持

每一页多产出一份 .md,站点根目录多一份 llms.txt,读者可以把当前页交给 ChatGPT 或 Claude。

HTML 页面里有侧栏、脚本与样式,模型读它要先剥掉这层外壳。OINK 让同一份内容再产出一份纯 Markdown:每页一个 .md,站点根目录一份 llms.txt 索引,页面上一个「复制 Markdown 文本」按钮。三者都是构建期产物,没有运行时服务,也不需要内容协商。

这三件事都要站点自己在 outputs 里声明,主题不替站点打开。另有两样同样需要显式打开的产物,服务于一次要读不止一页的 agent:每个栏目一份全文包,每种语言一棵导航树。

每页一份 .md

markdown 是 Hugo 的内置输出格式。把它加进需要的页面类型:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 markdown 时要把该类型原本有的格式(RSSprint)一起写全,漏一个就丢一种输出。

URL 规律是在页面 URL 后面接 index.md

页面 Markdown
/zh/docs/customize/agents/ /zh/docs/customize/agents/index.md
/zh/docs/customize/(栏目首页) /zh/docs/customize/index.md
/zh/(站点首页) /zh/index.md

每个 HTML 页的 <head> 里同时有一条发现用的链接,抓取工具不必推断 URL:

<link rel="alternate" type="text/markdown" href="https://oink.pgsty.com/zh/docs/customize/agents/index.md">

.md 的内容

不是把渲染好的 HTML 转回 Markdown,而是 你写的源码:front matter 换成一个 H1 标题加一段引用式摘要,其后是正文原文,shortcode 就地展开成各自的 Markdown 形态。

/zh/docs/customize/print/index.md 的开头
# 打印支持

> 单页交给浏览器的 Cmd/Ctrl+P,整个栏目用 print 输出格式合成一份连续文档。

---

LLMS index: [llms.txt](/zh/llms.txt)

---

单页打印不需要配置:外壳(侧栏、目录、顶栏、按钮)都带 `d-print-none`,浏览器的 `Cmd/Ctrl+P` 得到的是一份干净的正文。

原生 Markdown 形态的组件(提示块、表格、参数表、图片属性行、代码围栏、数据围栏)在 .md 里原样保留源码,模型读到的与你写下的是同一份内容。栏目首页在正文之后还会附一份 Section pages: 子页链接清单。

shortcode 形态各有确定的降级:徽章变成强调文本或链接,按键变成 Ctrl + K标签页变成一段段 **标签名** 小节,参数表变成条目列表。每个组件页的「输出形态」小节写了它自己那一行。

站点没有开 LLMS 输出时,上面那条 LLMS index: 不会出现:主题不指向未发布的文件。

llms.txt

llms.txt 是站点根目录的一份纯文本清单,告诉模型「这个站有什么、机器可读版本在哪」。给 首页 加上 LLMS 输出格式即可生成:

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

多语言站点每种语言各一份:/llms.txt/zh/llms.txt。内容是自动生成的站点索引:

/zh/llms.txt(节选)
# OINK

> 本地优先、仅依赖 Hugo 的技术文档主题

## Site index

- [Home page](https://oink.pgsty.com/zh/index.md)
- [文档](https://oink.pgsty.com/zh/docs/index.md): OINK 是一套本地优先的 Hugo 文档框架……
- [博客](https://oink.pgsty.com/zh/blog/index.md): Docsy 文章、OINK 工程实践与 OINK 发布注记

## Documentation index

- [简介](https://oink.pgsty.com/zh/docs/about/index.md): 一套从 Docsy 演化而来的本地优先 Hugo 文档框架……
  - [亮点特性](https://oink.pgsty.com/zh/docs/about/features/index.md): 逐条列出 OINK 与普通 Hugo 主题的差别……
  - [案例](https://oink.pgsty.com/zh/docs/about/showcase/index.md): 找到最接近你的生产案例……
- [快速上手](https://oink.pgsty.com/zh/docs/start/index.md): 从官方 OINK Starter 建立本地基线,再分层定制。

## Site locales

- [English](https://oink.pgsty.com/index.md)
- [简体中文](https://oink.pgsty.com/zh/index.md)

三段的来源:Site index 是本语言首页加站点主菜单(menus.main,条目有 Markdown 版就链 Markdown 版,带 description 的顺带写上);Documentation indexdocs 栏目的子栏目及其下一层页面,缩进表示层级,每行附上该页的 descriptionSite locales 是站点配置里的全部语言。指向站外的菜单条目(GitHub、issue 跟踪器)会被剔除:它们属于导航外壳,不是本站内容。

改进 llms.txt 的入手处是主菜单与各栏目首页的 description,不是这个模板。

全文包

每页一份 .md 适合已经知道自己要读哪一页的 agent;想通读整本手册的 agent 只能一页页爬。LLMSFULL 输出把这件事压成一个文件:每个顶层栏目一份 llms-full.txt,按阅读顺序装下该栏目的每一页。它是 OINK 0.8.0 的新增能力,栏目不主动要就不生成。

开关在栏目首页自己的 front matter 里,不在站点配置:

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

front matter 里的 outputs 会整体替换站点级列表,所以要把该栏目原本有的格式写回去:这里漏掉 markdownprint,栏目首页就少一种输出。front matter 按语言分开,双语站点要在 _index.zh.md 里同样写一遍,才有中文的全文包。

产物是每种语言一份,落在栏目根下——/docs/llms-full.txt/zh/docs/llms-full.txt。顺序就是侧栏与翻页器呈现的阅读顺序:docsbook 栏目声明了 data/docs_nav.json 显式树时以显式树为准,否则按内容树的 weight。侧栏里藏起来的页面(toc_hide)同样不进包。

每一页前面有一条带来源 URL 的分隔,其后的正文与该页自己的 .md 逐字节相同:

/zh/docs/llms-full.txt(节选)
================
Source: https://oink.pgsty.com/zh/docs/customize/print/index.md
================

# 打印支持

> 单页交给浏览器的 Cmd/Ctrl+P,整个栏目用 print 输出格式合成一份连续文档。

================
Source: https://oink.pgsty.com/zh/docs/customize/agents/index.md
================

# Agent 支持

Source: 指向该页的 Markdown 输出;页面没有 .md 输出时回退到它的 HTML 地址。

只有顶层栏目能带全文包。写在更深一层的栏目上会告警——「LLMSFULL output requires a top-level section」——并且什么都不产出:hugo server 照常能用,加了 --panicOnWarning 的发布构建则会停在这里。

只要有栏目开了全文包,llms.txt 就会多出一段 ## Full-text bundles,列出本语言的全部全文包:发现入口仍在 agent 本来就会抓的那个文件里。

本站的文档栏目已经开启:https://oink.pgsty.com/zh/docs/llms-full.txt 是全部中文文档,一次抓取。

侧栏是站点的目录,读得懂它的 agent 可以先规划路线再抓正文。NAVJSON 输出把它变成数据:每种语言一份 navigation.json,放在语言根目录下。和全文包一样,它是 OINK 0.8.0 新增、默认关闭,由站点在首页打开:

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

这会产出 /navigation.json/zh/navigation.json。这棵树就是侧栏与翻页器读的那一棵——docsbook 栏目声明了 data/docs_nav.json 显式树时以显式树为准,其余按内容树的 weight

/zh/navigation.json(节选)
{
  "baseURL": "https://oink.pgsty.com/",
  "language": "zh",
  "root": {
    "children": [
      {
        "children": [
          {
            "description": "每一页多产出一份 .md,站点根目录多一份 llms.txt……",
            "id": "/docs/customize/agents/",
            "kind": "page",
            "markdown": "https://oink.pgsty.com/zh/docs/customize/agents/index.md",
            "title": "Agent 支持",
            "url": "https://oink.pgsty.com/zh/docs/customize/agents/"
          }
        ],
        "id": "/docs/",
        "kind": "section",
        "title": "文档",
        "url": "https://oink.pgsty.com/zh/docs/"
      }
    ],
    "id": "/",
    "kind": "home",
    "title": "OINK",
    "url": "https://oink.pgsty.com/zh/"
  },
  "schemaVersion": 1
}
含义
id 去掉语言前缀的页面路径,同一页在每种语言里 id 相同
url 该语言下 HTML 页面的绝对地址
markdown 该页 .md 的绝对地址,只有页面确实产出 .md 时才有
title 导航标题(linkTitle,回退到 title
description 页面的 description,有才写
kind 真实页面是 homesectionpage;占位条目是 externallink
children 有序子节点,有子节点才写

数组顺序就是契约,weight 不会被序列化:顺序已经算好了,消费方再排一次只会与它来源的侧栏对不上。

占位条目保持侧栏里的样子:manual_linkexternal 节点,URL 照作者写的原样带出;manual_link_relreflink 节点,引用已经解析好。两者都没有页面身份,因此既没有 id 也没有 markdown。侧栏分隔线与 Hugo 从不渲染的页面会被略去,它们的子节点留在原位。

契约带版本:schemaVersion1,JSON Schema 随主题仓库发布,见 schema/nav.v1.schema.json——要消费这个文件就拿它做校验。站点发布了它时,llms.txt 的站点索引里会列出本语言的 navigation.json

本站已开启:https://oink.pgsty.com/zh/navigation.json 就是这棵树的实例。

页面上的 Agent 动作

面包屑行右侧的操作菜单里,跟 Agent 有关的是四条:

条目 做什么 出现条件
复制 Markdown 文本 抓取本页 .md 写进剪贴板(悬停时预取,点击后无明显等待) 本页有 markdown 输出
查阅 Markdown 源码 新标签页打开 .md 本页有 markdown 输出
在 ChatGPT 中打开 带一句提示词跳转到 ChatGPT assistant_links: true
在 Claude 中打开 同上,跳转到 Claude assistant_links: true

前两条只要开了 markdown 输出就存在。「复制」是拆分按钮的左半边(剪贴板图标),复制成功后短暂显示一个对勾。

后两条默认关闭,要显式打开:

hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      assistant_links: true

打开之后的边界:读者点击时,运行时用浏览器地址栏里的完整 URL(含真实域名、查询串与锚点)拼一句提示词,中文站是「请阅读 的内容,以便我就此向你提问。」,随后跳转到对方站点。离开本站的只有这个 URL,页面正文不会被上传,后续内容由对方自行抓取。URL 里不要放机密信息,站点也应当在隐私说明里披露这条第三方边界。

页面可以收紧站点策略,不能反向打开:front matter 里 page_context_menu: { assistant_links: false } 关掉本页的助手链接;站点没开时页面写 true 不会生效。整个菜单按页关闭用 page_context_menu: false,见页面参数

命令面板里也能搜到这两条助手动作(用的是同一份动作清单),见命令面板

按页面退出 .md 输出

在页面 front matter 里重写 outputs。它同样是整体替换,只写要保留的格式:

content/legal/terms.zh.md
---
title: 服务条款
outputs: [HTML]
---

要保留 RSS、只去掉 Markdown,就把其它格式列全:

content/blog/_index.zh.md
---
title: 博客
outputs: [HTML, RSS, print]
---

自定义输出

主题用 layouts/all.md 渲染 Markdown 输出,用 layouts/index.llms.txt 生成 llms.txt,两种可选输出则由 layouts/list.llmsfull.txtlayouts/index.navjson.json 负责。站点在自己的 layouts/ 下放同名文件即可整体替换,但 先考虑更窄的做法

  • 按内容类型layouts/blog/single.mdlayouts/docs/list.md 这样带类型的路径只影响那一类内容,主题的打印模板即按此分化(layouts/blog/single.print.html)。查模板查找顺序确认你的组合。
  • 按 shortcode:站点自己的 shortcode 可以加输出格式专属模板,让它在 Markdown 输出里给出更适合机器读的形式。
  • 按页面:少数高价值页面手写内容,成本低于改模板。

llms.txt 的内容由站点结构决定,改模板之前先确认问题不在主菜单或 description。替换 index.navjson.json 还意味着接手 nav.v1 契约:你自己产出的内容仍要能通过 schema/nav.v1.schema.json 的校验。

验证

hugo -d public
ls public/zh/llms.txt public/zh/docs/customize/agents/index.md
ls public/zh/docs/llms-full.txt public/zh/navigation.json   # 开了才有

线上或本地预览用 curl

$ curl -s http://localhost:1313/zh/docs/customize/agents/index.md | head -5
# Agent 支持

> 每一页多产出一份 .md,站点根目录多一份 llms.txt,读者可以把当前页交给 ChatGPT 或 Claude。

$ curl -sI http://localhost:1313/zh/llms.txt | head -3

$ curl -s http://localhost:1313/zh/docs/llms-full.txt | head -3
================
Source: http://localhost:1313/zh/docs/index.md
================

再检查四处:

  • 任一页 HTML 的 <head> 里有 rel="alternate" type="text/markdown"
  • 面包屑行右侧的复制按钮点击后粘贴,得到的是 Markdown 而不是 HTML;
  • llms.txt 里没有指向站外的链接;
  • 开了这两种输出的话:llms-full.txt 里每一页都以一行 Source: 开头,同一页在各语言 navigation.json 里的 id 相同。

限制

  • 主题产出的机器可读表面是四种构建期文件:每页 .mdllms.txt,以及需要显式打开的、每个顶层栏目一份的 llms-full.txt 与每种语言一份的 navigation.json。站点地图仍是 Hugo 自己的 sitemap.xml
  • 全文包属于顶层栏目,没有整站一份的 llms-full.txt:想读全站的 agent 按栏目逐个读,清单在 llms.txt 里。
  • LLMSLLMSFULLNAVJSON 都声明为非替代格式,所以它们都不会出现在 <head>alternate 链接里,也没有对应的页面操作;它们靠约定俗成的路径与 llms.txt 里的条目被发现。
  • 服务端内容协商(同一个 URL 按 Accept: text/markdown 返回 Markdown)不属于主题范围,要做在托管层。
  • Markdown 输出走 源码 路径:只在浏览器端由 JavaScript 生成的内容(运行时绘制的图表)在 .md 里是围栏源码,不是图。