这篇文章仅作者可见

写给我自己的备忘:这个博客怎么托管、怎么发布、写文章有哪些约定、读者能用哪些功能、出了问题去哪儿看。主题、配置、写作、运维都在这一篇里,不用再翻别的地方。代码层面的细节以仓库里的 AGENTS.md 为准;给读者看的那份是《致读者的一封信》——本站只有这两篇”说明书”。


一、快速开始

1. 环境

需要 Ruby(Homebrew 的 ruby,~/.zshrc 已把 /opt/homebrew/opt/ruby/bin 和 gems 的 bin 加进 PATH)和 Bundler;Node 只在编译样式和跑检查脚本时用到。

bundle install                    # 装 Gemfile 里的 Jekyll 4.4 及插件
npm install                       # 只为 node_modules/.bin/lessc(改样式时才需要)
pip install fonttools brotli      # 只为 tools/fa-subset.py(极少需要)
brew install webp lychee          # 图片转 WebP、本地跑链接检查(可选)

2. 日常命令

jekyll serve --future                       # http://localhost:4000,含未来日期的文章
jekyll serve --future --drafts              # 再加上 _drafts/
python3 tools/new-post.py slug "标题" --tags AI  # 新建文章骨架
jekyll build                                # 输出到 _site/(和线上一致:不含未来文章)
node tools/check-render.cjs <slug>          # 浏览器渲染检查,需要先起 Chrome,见第五节
npm run check                               # 推送前的全套检查(pre-push 钩子也跑它),见第七节
npm run css / npm run js                    # 改了 less/ 或 js/*.js 之后重新生成产物

jekyll build 时那条 Conflict: … the-productive-programmer-on-windows.html 警告是历史遗留,无害。

3. 目录结构

仓库目录结构
路径 内容
_posts/ 文章,YYYY-MM-DD-slug.md
_drafts/ 草稿,只在 --drafts 时预览,永不发布
slides/ reveal.js 幻灯片:/slides/xxx.html 落地页(缩略图 + 播放器 + 评论),/slides/xxx/play.html 全屏版,/slides/ 是索引
moments/ 随笔(碎碎念):一个月一个文件 YYYY-MM.md,/moments/YYYY-MM.html 是那个月的时间线,/moments/ 是最新一个月
_layouts/ _includes/ 模板(Liquid)
_data/series.yml _data/roadmaps.yml 系列文章定义(属于哪张地图、序号)与三张学习地图;/series.html 和地图文里的系列表格都从这里渲染,见第四节第 4 小节
img/in-post/ 文章图片(WebP)
less/ → css/ 样式源与产物(见第七节的注意事项)
js/ 前端脚本:目录、脚注 / Tips、评论与划线、搜索、图放大、代码复制
tools/ 维护脚本、检查脚本、评论系统的 Cloudflare Worker
.github/workflows/ deploy(发布)、check(质检)、links(每周外链)

二、托管:GitHub Pages 是什么、能做什么

1. 基本事实

  • 仓库 arganzheng/arganzheng.github.com,master 分支就是站点源码;根目录 CNAME 文件写着 arganzheng.life,GitHub 据此接受这个域名。DNS 是四条 A 记录指到 GitHub Pages 的 IP(185.199.108-111.153)。
  • 强制 HTTPS 已开(仓库 Settings → Pages → Enforce HTTPS):http:// 会 301 到 https://。证书由 GitHub 自动签发和续期,不用管。
  • GitHub Pages 是纯静态托管:没有服务端代码、没有数据库、不能自定义 HTTP 头。所有”动态”的东西(评论、点赞、阅读数)都靠浏览器直接调外部服务实现,见第六节。
  • 软限制:仓库 1 GB、站点 1 GB、每月 100 GB 流量、每小时 10 次构建。个人博客离这些很远;图片已经全部转成 WebP(img/ 从 24 MB 降到 10 MB)。
  • 缓存:GitHub Pages 对所有文件(包括 HTML)返回 Cache-Control: max-age=600,无法修改。所以一次改动从 push 到所有读者看到,最坏是 3 分钟构建 + 10 分钟浏览器缓存。按 F5 普通刷新会立即重新校验 HTML;CSS/JS 的 URL 带构建时间戳(?v=…),HTML 一新它们必然跟着新。这个 13 分钟我认为可以接受,所以没有再在前面加 Cloudflare 之类的 CDN 来改缓存头。

2. 构建与发布:GitHub Actions 而不是老式 Pages 构建

Pages 有两种模式。老式(legacy)是 GitHub 自己用 Jekyll 3.10 和白名单插件构建;本站已切到 Actions 模式(Settings → Pages → Source = GitHub Actions),由 .github/workflows/deploy.yml 用 Gemfile 里锁定的 Jekyll 4.4 构建后发布,好处是本地、CI、线上是同一个 Jekyll,也不再受插件白名单限制。

触发时机:

部署的触发时机
触发 说明
push 到 master 约 3 分钟后上线
每天北京时间 00:05 定时构建,让未来日期的文章按日期自动上线(构建不带 --future)
手动 Actions → deploy → Run workflow

所以定时发布的用法就是:文件名和 front matter 用未来的日期,push 上去,到那天凌晨自动出现。本地预览要看到它们需要 jekyll serve --future。

_config.yml 里显式写了 timezone: Asia/Shanghai,”今天”按北京时间算。

3. 常用检查入口

  • Actions 页面:deploy(发布)、check(每次 push 的质量检查)、external links(每周一外链检查)。
  • gh run list --limit 5 命令行看最近几次结果;gh run view <id> --log-failed 看失败原因。
  • Pages 设置状态:gh api repos/arganzheng/arganzheng.github.com/pages。
  • 作者仪表盘 /admin/stats.html(不在导航、不进 sitemap、noindex):阅读趋势(/views/daily,7 / 30 / 90 天柱状图 + 这段时间读得最多)、文章榜(/stats/top:阅读 · 点赞 · 点赞率 · 分享 · 评论,点表头排序,默认 TOP 10 可展开)、修订简报(见下)、读者划出来的句子(/reactions/top:点赞最多 / 分享最多,带章节,点句子直达原文)、最近评论(用评论框同一个 GitHub 登录读 Discussions)、待处理的勘误 Issue 与死链报告、值得翻新的老文章(三年以上没更新、还在被读的技术文章,按阅读 × log(年龄) 排,每行带简报和 GitHub 编辑入口;写上 updated: 就从榜上消失)。数据全是公开的,只是集中在一页。
  • 修订简报:仪表盘里选一篇文章(或点文章榜每行的「简报」,URL 形如 /admin/stats.html#brief=/slug.html——只有这一个入口),把这篇文章的全部反馈汇成一份 Markdown。选文章不用翻下拉框:简报上方的待修订的文章列出带有未处理反馈的文章——开着 划线评论 报错 Issue,或已被 Action 开成 待修订 Issue——按 feedback-brief.js 同一套打分排,每行带简报和编辑入口。简报本身包含:GET /feedback?path= 中的点赞和章节、Discussion 的划线评论 / 回复(走 Worker 的匿名 /discussions 中转)、划线评论 标签的 Issue 及其 open/closed 状态(REST),再抓一次文章页判断每条引文还能不能定位。段落按 点赞 + 评论 + ▲ + 3×建议修改 排序,已修正的和定位不上的分开列,末尾附一段给 AI 的修订指令。点「复制 Markdown」贴给 AI 即可;AI 改完提交时写 Fixes #N,文章上的标记自动变绿。
  • 待修订队列(自动):.github/workflows/feedback-queue.yml 每周一早上(或手动,参数 threshold / only / dry_run)跑 tools/feedback-queue.cjs,用和仪表盘同一份聚合代码(js/feedback-brief.js,浏览器和 Node 都能加载)给全站每篇文章算一遍简报:分数 = 待处理段落分 + 未处理的普通评论,达到阈值(默认 3)的文章开一个 待修订 标签的 Issue,正文就是简报;下周反馈有变化就更新正文,全部处理完(分数掉到阈值以下)就自动关闭。这样 Devin / Copilot 一类工具可以直接从 Issue 接活,我不用记得去看仪表盘。仪表盘「待处理」区第一栏列的就是这些 Issue(带「简报」直达链接)。本地试跑:DRY_RUN=1 API=http://localhost:8788 GITHUB_TOKEN=$(gh auth token) node tools/feedback-queue.cjs(先 npm i --no-save linkedom@0.18.13)。

三、站点配置(_config.yml)

站点级的开关都在这个文件里,现状如下:

title: Arganzheng's Blog                 # 导航栏和首页显示
SEOTitle: 阿甘的博客 | Arganzheng's Blog   # <title> 用这个,可以和 title 不同
description: "…"                          # 首页描述、分享卡片兜底
url: "https://arganzheng.life"            # 绝对链接、canonical、feed 都用它,必须 https
timezone: Asia/Shanghai
permalink: /:title.html
paginate: 10                              # 首页每页文章数

# SNS:填了就出现在页脚
github_username / zhihu_username / linkedin_username / wechat_qrcode   # weibo / twitter / facebook 注释掉了

# 侧栏(首页、列表页右侧;窄屏时挪到底部)
sidebar: true
sidebar-about-description: "…"            # 头像下一句话
sidebar-avatar: /img/avatar-argan-msup.jpg
featured-tags: true                       # HOT TAGS 模块
featured-condition-size: 2                # 文章数 > 2 的标签才上榜
related_posts_threshold: 3                # 文末「YOU MIGHT ALSO LIKE」条数(算法见第六节「其他」)
recommends:                               # 侧栏 RECOMMEND:title / href / desc(一句推荐理由),外链新窗口打开
  - { title: …, href: …, desc: … }
# friends: [...]                          # 友链模块,模板支持但没配

# 统计:GA4 gtag、百度统计(_includes/analytics.html);Cloudflare Web Analytics 直接写在 head.html
ga_track_id: 'G-…'
# ba_track_id:

# 评论与划线(第六节)
giscus:                                   # 仓库 / 分类的 ID,只用来定位讨论串和 GitHub 登录
annotations:
  api: https://blog-annotations.arganzheng.workers.dev   # 留空 = 关闭整个评论功能
  issues: true                            # 编辑器里显示「同时提交 Issue」

service-worker: false                     # 见第七节,不要开
plugins: [jekyll-paginate, jekyll-sitemap, jekyll-redirect-from]

早年用过 Disqus 评论和 JiaThis 分享按钮,都已移除:Disqus 在国内被墙且往正文里注入广告链接;JiaThis 早已停服。评论现在是 GitHub Discussions,见第六节。


四、写文章

1. 三种文章布局,以及它们和幻灯片的区别

三种文章布局
layout: 头部 用途 现存文章
post 无大图,白底标题 + meta 行 默认,所有技术文章 370+
header-post 全宽背景图(或 CSS 渐变)+ 白字标题,导航栏反白 随笔、生活类,想要一张封面时 3 篇(2017 年的随笔)
keynote 头部是一个 iframe,嵌一份在线幻灯片,正文在下面 一次分享的「幻灯片 + 文字稿 + 评论」合在一页 1 篇(演示)

keynote 和 slides/ 目录下的幻灯片是两回事:slides 布局是幻灯片本身(slides/xxx.md 用 Markdown 写、reveal.js 渲染,URL /slides/xxx.html 是带缩略图、播放器和评论区的落地页,/slides/xxx/play.html 是全屏演示版,见下文第 10 小节);keynote 是一篇文章,只是把某个幻灯片 URL(自己的 /slides/xxx/play.html 或外部的 Slides.com / Speaker Deck)嵌在头部,下面可以写讲稿、参考资料,也有评论区和阅读数。幻灯片落地页自己就有评论和统计,所以只有要配长篇讲稿时才需要 keynote 文章——《keynote 布局演示》就是一篇活的例子。注意 keynote 页面的标题、标签只出现在 <title> 和列表里,页面头部就是幻灯片本身。

2. front matter 全部字段

文章放在 _posts/YYYY-MM-DD-slug.md,URL 是 /slug.html(permalink: /:title.html;改文件名 = 改 URL = 评论串和外部链接都会断,非改不可时加 redirect_from)。

---
layout: post                  # post | header-post | keynote
title: "标题"                  # 系列文章约定「系列名(NN):副标题」
subtitle: "副标题"             # 可选;也是分享卡片 / 搜索引擎描述的兜底
date: 2026-09-09 14:30:00     # 可选;文件名已含日期,同一天多篇想控制顺序时再写时间
tags: [AI, AI-Infra]          # 必填:文末推荐、HOT TAGS 都靠它;先复用已有词(/tags/ 上有全表),大小写要一致
catalog: true                 # 右侧浮动目录;正文里写 [TOC] 也会自动开启
series: deep-dive-into-vllm   # 系列文章才写,key 见 _data/series.yml
updated: 2026-09-20           # 大改后写上:头部显示「更新于」,JSON-LD dateModified;也是「本文写于 N 年前」提示和仪表盘「值得翻新」榜的依据
stale: false                  # 可选:常青文章不显示「本文写于 N 年前」提示(默认 3 年以上的技术文章都显示)
description: "一句话摘要"       # 分享卡片 / 搜索引擎;不写用 subtitle,再不写用正文开头
author: arganzheng            # 可选,默认 arganzheng
published: false              # Jekyll 内建:不构建这篇(比放草稿目录更方便临时下线)
visibility: private           # 作者专用:仍构建,但不进入读者浏览与搜索;页面需作者 GitHub 登录
pinned: true                  # 置顶:首页第一页排在最前,带「置顶」徽章;可多篇,按日期排
category: life                # 分类:不写 = 技术文章(首页 Tech);life = 随笔/生活(不进首页,进导航 Life 的 /life/ 卡片页)
redirect_from: /old-slug.html # 旧地址 301 过来(jekyll-redirect-from),可写数组

# 仅 header-post:
header-img: img/post-bg-2015.jpg                          # 背景图;也是分享卡片的图
header-bg-css: "linear-gradient(to right, #24b94a, #38ef7d)"  # 用 CSS 渐变代替图
header-mask: 0.3              # 图上压一层黑色遮罩的透明度,字看不清时用
header-img-credit: "Unsplash" # 右下角「Image by …」
header-img-credit-href: "https://unsplash.com/photos/xxx"
# 选图:标题和 meta 是白字,所以要深色或加 header-mask;宽度 ≥ 1920,体积控制在 300 KB 内(首屏就加载)

# 仅 keynote:
iframe: "/slides/reveal-demo/play.html"   # 嵌入的幻灯片地址(站内用全屏版 play.html)
navcolor: invert              # 幻灯片是浅色背景时,把导航栏文字变深色
---

visibility: private 与 published: false 不同:页面照常构建并部署,但从首页、归档、标签、feed 和搜索中移除,并标记 noindex、sitemap: false;导航里的锁图标会进入 /private/ 私有文章目录(页尾有「作者仪表盘 →」),页面正文要在页面上用作者 GitHub 账号登录后才能显示。仓库里的源文件和已部署 HTML 仍然公开,浏览器标签页标题也仍可见,所以这只是对读者隐藏,不是保密措施。私有页只读 /views,不 POST 计数,因此不会增加文章榜阅读数;上一篇 / 下一篇会跳过私有文章。blogAuthor 会缓存在本机,登出或 session 过期 / 验证失败会清除。其他 published: false 文章不受影响。以后新增列表 / 搜索插件只遍历 site.posts,不要遍历全部 collections,否则会把私有文章重新列出。CI 的 check-render 检查本文时会先排除行内 <code> 再检查图片语法,代码示例 ![alt](/img/in-post/x.webp) 不会误报成未渲染图片。

不需要的字段:mathjax: true(公式自动检测、KaTeX 渲染,没有开关)、header-style: text(post 布局就是纯文字头部)、nav-style(这里叫 navcolor,只有 keynote 用)。同类 Jekyll 主题常见的双语切换(multilingual / lang)这里没有。

文章头部那一行「Posted by … 日期 · 更新于 约 N 分钟 · X.Xk 字 阅读 · 点赞 · 评论 · 分享 四组文字统计」是自动的:阅读时长按去掉代码块后的字数 / 450 字每分钟估算。

3. 分类:Tech / Life

只有两类,用 category: 区分,不写就是技术文章(绝大多数),走首页(导航 Tech)的分页流。life 是随笔、瑜伽、生活:不进首页,导航 Life 进 /life/ 卡片页(用各篇的 header-img 做封面,所以生活类建议用 header-post 布局配一张图),归档页标 [Life]。首页的分页由 _plugins/home_flow.rb 把 life 和置顶文章标成 hidden(jekyll-paginate 只对这个字段生效,归档 / 标签 / RSS / 搜索不受影响),所以每页都是满的、不会有空洞。备忘录、读者信、布局演示这类关于博客本身的文章就是普通技术文,不单独分类。RSS 只有一份,每条 <category> 里带 tech 或 life。

4. 系列文章

系列信息不写在正文里,写在 front matter 的 series: 字段,取值是 _data/series.yml 里的 key。同系列文章按日期排序,页面自动生成四样东西:文首的系列盒子(一行「系列 《…》 第 N / X 篇 · 目录」,点开是全部篇目,下面一行上一篇 / 下一篇)、宽屏右侧 OUTLINE 下面的「本系列」列表(随文滚动,当前篇加粗)、文末的系列目录、以及把 Previous / Next 换成系列内的上一篇 / 下一篇。

新开一个系列:先在 _data/series.yml 加一条,总览文章本身不写 series:。标题约定是「系列名(NN):副标题」,导航里只显示「):」后面的部分。一条完整的定义:

deep-learning-foundations:
  name: 深度学习基础:从反向传播到残差
  overview: /deep-learning-foundations.html   # 总览页 URL
  roadmap: ai-algorithm                       # 属于哪张地图:ai-algorithm | ai-infra | ai-application;不写 = 不在任何地图上
  number: 4                                   # 在这张地图里的序号(纯元数据;L0–L7 的层次是内容,写在地图文里)
  # shared_with: [ai-algorithm]               # 另一张地图也读它(Python / PyTorch / Transformer 三个系列,家在 Infra 地图)

三张地图本身在 _data/roadmaps.yml(key → name / short / url),顺序就是 /series.html 上的顺序。

/series.html(导航栏 Series)是全站系列的一页树:地图 → 系列 → 文章,原生 <details> 折叠,地图默认展开、系列默认收起;不在地图上的系列归最后一张「其他系列」。文内的系列引用和文末目录链到 /series.html#<key>,会自动展开到那个系列。它是一个 .html 文件而不是 /series/ 目录,因为 lychee 查不了目录索引上的锚点(/tags/#x 也是同一个限制)。篇数按当天已发布算(普通构建不含未来日期),未发完的显示「已发 3 / 9 篇」、一篇没发的显示「即将发布 · 9 篇」,都是 _plugins/series_pages.rb 算的。

三篇地图文里的「系列总览」表格不要手写数字,每行是一个 include(层次作为参数留在文章里):

{% include series-row.html key="cpp-for-ai-infra" layer="L1" %}
{% include series-row.html key="python-for-ai-infra" layer="L1" note="(与算法地图共享)" %}
{% include series-row.html key="math-for-ai" layer="L0" cols="layer,link,count" %}   # 列可配,默认 no,link,layer,count,hours

序号、名称、链接来自 series.yml;篇数与时长来自构建时直接扫 _posts/*.md(未来日期的也算进「计划」,末尾的「系列总结与通关自测」不计),连载中显示 3 / 9。时长按 450 字/分钟、含代码、去掉 HTML 标签算,常数是 series_pages.rb 里的 CHARS_PER_MINUTE,改它一处全站一致;Infra 地图文里「主线合计约 N 小时」那句也是 Liquid 算的。历史教训:原来手写的时长(Infra 主线 203h)是按这个规则算出来的 3–4 倍,几乎肯定是把 UTF-8 字节数当了字数,所以以后不要再手填。

非系列文章的文末 Previous / Next 仍是按时间的相邻文章,另有按 tag 推荐的「YOU MIGHT ALSO LIKE」。

5. 新建、草稿与本地预览

python3 tools/new-post.py my-slug "标题" --tags AI,AI-Infra --series deep-dive-into-vllm   # 生成 _posts/今天-my-slug.md
python3 tools/new-post.py my-slug "标题" --draft                                          # 生成 _drafts/my-slug.md
jekyll serve --future            # http://localhost:4000,含未来日期的文章
jekyll serve --future --drafts   # 再加上 _drafts/ 里的草稿

草稿放 _drafts/(文件名不用带日期),只有加 --drafts 时才会出现在本地预览里,线上永远不发布;写完移到 _posts/ 并加上日期即可。new-post.py 会校验 --series 的 key 是否存在。

一个系列整体先写成草稿时(如《扩散模型推理基础设施》十篇),系列内的顺序按日期排,而草稿没有文件名日期、默认取文件修改时间,顺序会乱——所以草稿的 front matter 里暂时写 date:(只为排序,正式发布时删掉、改成文件名日期),预览用 jekyll serve --drafts --future --port 4001 -d /tmp/_site4001,再用 SITE=http://localhost:4001 node tools/check-render.cjs <slug> 检查渲染。地图与总览里指向草稿的链接要等发布时再加(lychee 只认 _site 里存在的页面)。

6. Markdown 能力速查

kramdown(GFM 模式),加上博客自己的扩展:

Markdown 能力速查
你写的 效果
```python title="标题" 等围栏代码 rouge 高亮;块顶一条 header 栏:左「代码块 N:标题」(没写语言或 ```text 的叫「文本块 N」,各自编号),右语言标签 + 复制 / 评论按钮;title="…" 可不写,但它是这段代码的划线锚点(见第 8 小节);过宽的代码块横向滚动;≥ 2 行的块都带行号(```text 和没写语言的也一样;块前一行写 {:.lineno} / {:.no-lineno} 可单独开关)。行号是 CSS 画的,复制、划线引用都不会带上它。代码里单独一行 # !ref 名字 + 正文 [文字](#名字) 把说明和代码行绑起来,见下一小节末
Markdown / HTML 表格 + 表后一段 Table: 标题 GitHub 风格,下方居中「表 N:标题」(必写,见第 8 小节);右上角复制菜单支持 TSV / Markdown / HTML,也可从表题进入反馈
```mermaid ,首行 %% 图:标题 Mermaid 图,浏览器端渲染,下方「图 N:标题」(必写);右上角放大 / 缩放 / 拖动
<div class="code-tabs" markdown="1"> 包住两三个围栏代码块 同一段代码的多语言 tab(Python / Java / C++…),见本节末
$$ ... $$ KaTeX 公式(行内和块级都用 $$),有公式的页面才加载 KaTeX
![alt](/img/in-post/x.webp) alt 就是图题(必写),下方「图 N:alt」;自动 lazy 加载,右上角放大
概念[^名字] + [^名字]: 解释 浮窗脚注:悬停编号就地弹出卡片,见下一小节
[概念](# "tip: 一句话解释") 行内 Tips:虚线下划线 + ? 角标,见下一小节
站外链接 自动加虚线下划线和 ↗ 图标、新窗口打开
引用块第一行写 [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] GitHub 风格彩色提示块(Callout),见本节末
[TOC] 就地生成目录,同时开启右侧浮动目录
<i class="fa fa-xxx"></i> Font Awesome 4.7 图标(约 150 个常用的已内置,见第七节)

多语言代码 tab(code-tabs):把同一段代码的几个语言版本写在一个 <div class="code-tabs" markdown="1"> 里(markdown="1" 让 kramdown 照常高亮里面的围栏),每个直接子代码块变成一个面板,标签从 language-xxx 取(python → Python、java → Java、cpp → C++,其余按名字)。js/code-tabs.js 的规则:选择是页面级的——点一次 Java,全文所有组切到 Java;并记进 localStorage["code-tab-lang"],下一篇打开时默认就是 Java;某组没有这个语言就显示它的第一个面板。没有 JS 时 CSS 把面板堆叠显示、各标语言名。「复制为公众号格式」导出时去掉 tab、每个面板前加一行语言名。样式在 less/extras.less 的 .code-tabs 段,由 npm run css 生成 CSS。用在《面试手撕代码》的算法篇(Python / Java)与 Infra 篇(Python / C++)。

提示块(Callout):引用块第一行写 [!类型],构建时 _plugins/callouts.rb 把它换成带图标和标题的彩色框。写法和 GitHub 的 alert 一样,所以在 GitHub 上看源文件也是同样效果:

> [!NOTE]
> 不写标题就用默认标题「说明」。
>
> - 里面照常写列表、代码块、链接、公式

> [!WARNING] 别在生产环境这么做
> 类型后面同一行的文字会替换默认标题。

效果:

这是一个提示块

类型后面同一行的文字就是标题。

五种类型和默认标题:NOTE 说明(蓝)、TIP 提示(绿)、IMPORTANT 重要(紫)、WARNING 注意(黄)、CAUTION 警告(红)。类型不区分大小写。注意:

  • [!类型] 必须是引用块的第一行。不认识的类型(比如拼错成 [!NOTES])和普通引用块都原样保留、不报错,所以写完要看一眼渲染结果。
  • 只处理 _posts/ 里的文章;幻灯片、随笔、普通页面里写了不会转换。
  • 标题行不能划线评论(和图题、表题一样),正文可以。暗色模式有单独的配色。样式在 less/callouts.less,暗色在 less/dark.less。
  • 别拿它替代脚注和行内 Tips:提示块是给所有读者看的「停一下」,一篇文章用几个就够了。

7. 浮窗脚注与行内 Tips(作者给读者的解释)

这是本站最常用的两个”不打断阅读的解释”手段。原则:解释长(多段、代码、表格、列表)用脚注,一两句话用行内 Tips。

浮窗脚注就是标准 Markdown 脚注,只是读者悬停编号时就地弹出卡片、点击才平滑跳到文末(并避开吸顶导航栏);脚注内容支持完整 Markdown。看一段实际效果——悬停下面的编号:

在现代分布式深度学习架构中,集群通信与算子实现至关重要。业界广泛采用 NCCL 集合通信库1,并借助 Ring AllReduce 算法2实现跨卡梯度同步。在服务层,vLLM 引擎3引入了 PagedAttention 显存优化。底层算子常通过 PyTorch C++ 扩展骨架4开发;混合并行切分时要评估各并行策略的显存与通信模式5。

对应源码(脚注定义放文末任意位置,缩进四格可以放代码块和表格):

业界广泛采用 NCCL 集合通信库[^nccl],……开发高性能融合算子[^kernel-code]。

[^nccl]: **NCCL**:英伟达的集合通信库……详见 [NCCL 官方仓库](https://github.com/NVIDIA/nccl)。

[^kernel-code]: **PyTorch C++ 算子实现骨架**:
    ~~~cpp
    #include <torch/extension.h>
    torch::Tensor custom_add(torch::Tensor a, torch::Tensor b) { return a + b; }
    ~~~

行内 Tips 有四种写法,效果一样(虚线下划线 + ? 角标,悬停或轻触弹出;支持加粗、代码、链接、公式,不支持代码块和列表):

行内 Tips 的四种写法
写法 示例 效果
Markdown 链接 title(推荐) [GIL](# "tip: 全局解释器锁……") 在 Python 中,GIL 是多线程计算的主要制约
kramdown IAL [MVCC](#){: .tip data-tip="……"} MVCC 是高并发隔离的核心机制
Liquid include(可带「了解更多 ↗」外链) {% include tip.html text="RDMA" tip="……" url="https://…" %} RDMA 是消除传输瓶颈的基石
原生 HTML <span class="inline-tip" data-tip="……">词</span> 计算与通信重叠 能显著提升 MFU
公式也行 [AllGather 通信量](# "tip: 每卡发送 $\frac{N-1}{N} S$") AllGather 通信量 可精确量化

交互细节(不用记,知道有就行):悬停 100 ms 后才弹出防误触;鼠标移入卡片可以复制文字、点里面的链接;空间不够自动翻到下方;Esc、点空白处、鼠标移开都能关;手机上轻触弹出、再触关闭。

外链不需要任何标记:PyTorch 官网 这样的站外链接自动带 ↗ 并新窗口打开,归档 这样的站内链接保持原样。

问答脚注([^q0]、[^q1]……)是脚注的一个变种,专门给系列文章「文中提问、文末作答」用:文章抛给读者、随后由正文回答的每一个引导问题——文首的粗体核心问题(按「?」拆开)、列表里的「为什么……?」、章节开头 > **…?** 的导入句、总纲分章导读与路线图逐层说明里的「核心问题:」、小结或「回答核心问题」一章回看的问题——各挂一个 q 开头的脚注,按阅读顺序编号,文末逐条作答并链到对应章。不算引导问题、不挂脚注的:问句形式的标题(正文就是答案,加标记还会改掉标题的锚点)、自测 / 练习 / 面试题(已有折叠答案)、紧跟着「结论」段的「核心问题」(系列回顾篇)、表格单元格与代码 / 输出里的问号、修辞性反问。答案要可核对(数字、条件、是 / 否加理由),不能写「见下文」。tools/qfootnotes.py --check 校验每个 [^qN] 恰有一条非空定义、无多余定义、编号按阅读顺序;在前文补问题后用 --fix 重排编号。与浮窗脚注的区别只在前缀,规则要记牢:

  • 脚注名以 q 开头([^q0]… → id="fn:q0")的一律跳过浮窗:js/inline-popups.js 的 processFootnotes() 对 /^fn:q\d+$/ 的编号不加 has-popup-footnote、不绑悬停事件,悬停无卡片;点编号只平滑跳到文末的那条脚注(沿用已有的 scrollToTargetWithOffset,避开吸顶导航栏),文末的 ↩ 照常跳回。答案是读完再看的,不该悬停就泄底。
  • 文末脚注列表加了分隔线与标签:默认「脚注」,页面里只要有 fn:q… 就改标「本文引导问题答案」(.footnotes:has(li[id^="fn:q"])::before,样式在 less/extras.less 末尾)。
  • 解释性脚注不受影响:名字不要以 q 开头([^nccl]、[^ring] 这样即可),继续浮窗。所以一篇文章里两种脚注可以并存,只靠名字区分。
  • 写法:答案一到几句、可核对、含数字,末尾「详见 [第 N 章](#锚点)」链到章标题(锚点是 kramdown 生成的 <h2 id>,从 _site 里抄);定义放「下一篇」之后即可,kramdown 渲染时本来就把脚注放在正文末尾。

代码引用(逐行讲解代码时用):把正文里的一段说明和代码块里的某几行绑起来,读者从任何一边都能找到另一边。以前的做法是在代码注释里写 ①②③、下面列表逐条对应——读者还是得自己在两边找数字,而且 ① 会跟着代码一起被复制走。现在两步:

  1. 代码里,在要指的那行上面单独加一行注释 # !ref 名字。名字 用短英文(base、to-dev),一篇文章里不重复;要盖住连续几行就写 # !ref 名字 +N(再多包 N 行)。注释符按语言写://、--、;、/* */、<!-- --> 都认。
  2. 正文里,用普通链接 [文字](#名字) 指过去——文字就是自然的话(「取 batch 那一行」「loss.backward()」),不用写行号。

页面上:悬停正文的引用 → 代码里那几行亮起(淡蓝底、左侧蓝条);点击 → 不在视口时滚过去并闪一下。反过来,代码左侧被引用的行号是蓝色的,悬停(手机上轻触)它 → 就地弹出正文那段说明,像脚注一样。指令行本身在渲染时被删掉:不占行号、复制不带、搜索不索引,读者看到的是干净代码。看效果——悬停下面的划线文字,或悬停代码左侧蓝色的行号:

def fact(n):
    if n == 0:
        return 1
    return n * fact(n - 1)   # 没有尾递归优化,
                             # n 大了会 RecursionError

基例在 n == 0 时返回 1,其它情况递归乘下去——Python 不做尾递归优化,所以 fact(2000) 会碰到默认 1000 层的递归深度限制。

源码长这样(下面这个块前面写了 {:.no-refs},指令才得以原样显示——展示语法时才需要):

    # !ref base
    if n == 0:
        return 1

[基例](#base)在 `n == 0` 时返回 1,其它情况[递归乘下去](#rec)……

要记的几条规则:

  • 有 !ref 的块自动带行号(```text 也一样),因为蓝色行号就是标记。
  • 名字一篇文章内唯一;同一段代码的多语言 tab(code-tabs)各写一份同名是允许的,页面上联动到当前显示的那份。别的文章也能链过来:[..](/slug.html#名字)。
  • 指令只认独占一行的写法,写在代码行尾(x = 1 # !ref a)不算;写在多行字符串 / 块注释内部的也不算(当普通文本)。
  • 写了 !ref 却没人引用 → jekyll build 打 warn;正文引用了不存在的名字 → npm run check 的链接检查会红。
  • 没有 JS(RSS 阅读器、打印)时就是普通锚点:点链接跳到那行。公众号导出会在引用文字后补「(第 N 行)」。

第一篇用上它的文章是《PyTorch 使用层(上)》第六章的二十行训练循环(逐行解释),可以对着看。

8. 图片与表格:都要有标题

原则:每一张图、每一张表都必须有标题,页面上统一显示为图 / 表下方居中的「图 N:标题」「表 N:标题」,编号自动生成(图、表各自一套)。图片的标题就是 alt,Mermaid 图是源码首行的 %% 图:…,表格是表后一段 Table: …(写法见下)。标题写成名词短语、说清“读者在看什么”(「三种并行方式的通信量对比」),不重复小节标题、不加句号、不自己写编号。页面上看到光秃秃的「图 N」「表 N」就是漏了标题,当 bug 修。为什么这么要求:标题是这张图 / 表的划线锚点——读者的赞 / 评论、修订简报里的引文都落在这行文字上,只有编号的话中间插一张就全变。

新图先放 img/in-post/,大于 20 KB 的 png/jpg 跑一次 python3 tools/webp-images.py --apply 会转成 WebP 并自动改写引用(先不带 --apply 是预览)。手绘 SVG 直接放,不用转。文章里用绝对路径 /img/in-post/xxx.webp。

alt 必填,而且要像图题(![学习率调度与 batch 增长](…))。js/figures.js 会把独占一段的图片包成 <figure>,下方居中显示「图 N:alt」作图题,鼠标移到图上时右上角浮出一条小工具条(放大 · 评论;表格是复制 · 评论;触屏常显)——点「评论」就选中图题、弹出划线工具条,所以图题就是这张图的划线锚点:读者对图的赞 / 评论、简报里的引文,都是这行 alt。没有 alt 只剩「图 N」,中间插一张图编号就变、旧评论会「未定位」。Mermaid 图同样有图题:源码第一行写 %% 图:xxx(或 Mermaid front matter 的 title:),否则只显示「图 N」;它的方块在复制按钮左边。

表格标题与工具:正文表格和图片一样有表题——表格下方居中显示「表 N:标题」,表 N: 自动按出现顺序编号(与图的编号各自独立),没有标题时只显示「表 N」。标题用 Pandoc 的写法:表格后空一行,写一段以 Table: 或 表: 开头的文字(表1: 也认,编号会被丢掉换成自动的;可以带行内代码等标记):

| 调度器 | 特点 |
|---|---|
| … | … |

Table: 各调度器对比

构建时 _plugins/table_captions.rb 会把这段折进表格变成真正的 <caption>,所以 RSS、微信导出、review 页面里也有标题;HTML 表格直接写 <caption>标题</caption> 即可。右上角的反馈按钮:有标题就选中标题(表题就是这张表的划线锚点);没有标题时选中表头,没有表头才降级为整表。复制菜单提供 TSV(表格软件)、Markdown 和 HTML 三种格式。

代码块 / 文本块的标题:围栏块(有语言的叫「代码块 N」,没写语言或 ```text 的叫「文本块 N」——命令输出、日志、目录树都算,两套编号各自独立)不像图、表那样在下方放题,而是块顶一条 header 栏,VitePress 风格:左边「代码块 N:标题」,右边语言标签和复制 / 评论按钮,触屏也常显。标题写在围栏那一行,MDX 的写法:

```cpp title="Dispatcher::call 的完整签名"
...
```

```text title="第 3 个 epoch 起 loss 开始抖"
epoch 3  loss 0.41
```

kramdown 本身不认围栏语言后面的文字(会把整块变成一段普通文字),构建时 _plugins/code_titles.rb 先把 title= 挪到块前的 {: data-title="…"} 上,再由 js/figures.js 画出 header。标题就是这个块的划线锚点:点 header 上的评论按钮就选中标题,读者的赞 / 评论落在标题上,代码怎么改都不断;没写标题的块选中第一行代码(不再是整段——以前引文是整段代码,改一行旧评论就「未定位」),第一行不改就还在。标题不强制(老文章几百个代码块),但示例代码、跑过的日志值得写一个:一个好标题说清“这段代码 / 输出在证明什么”,读者在评论和简报里看到的也是这一句。{:.no-lineno} 之类的 IAL 照常写在块前一行,两者会合并。

9. Liquid 陷阱

{% 和 {{ 出现在代码里(PTX、Go template、Jinja)必须包在 {% raw %}…{% endraw %} 里,否则 Liquid 会把它当模板语法,构建直接失败——写这一段本身就让构建失败了一次。raw 不能嵌套。

10. 幻灯片

slides/xxx.md 用 layout: slides 是 reveal.js 演示文稿,/slides/ 是索引页。一个文件出两个页面:

  • /slides/xxx.html 落地页(网页版 PowerPoint 的样子):左边一栏缩略图,每页一张小图(就是那页的内容按 1280×720 缩小,深色背景页也是深色),当前页高亮并自动滚到可见处,点哪张右边就翻到哪页;右边是 16:9 的播放器,下面一条工具栏——上一页 / 下一页 / 页码 / PDF / 新窗口 / 全屏;鼠标在播放器上或选中左侧缩略图时 ← → ↑ ↓ 空格翻页、Home/End 跳首尾页、F 全屏,地址栏的 #/N 记着当前页,可以直接分享到某一页。上面是文章式的头部(作者 · 日期 · 页数 · 阅读 / 点赞 / 评论),下面是点赞 / 分享操作栏和评论区。阅读数、点赞、评论都记在这个 URL 上,和文章一样。窄屏时缩略图变成播放器下方的一条横向滚动条。
  • /slides/xxx/play.html 全屏版:自动生成,就是原来的纯 reveal.js 演示,投屏、?print-pdf 导出 PDF(播放器上的 PDF 按钮)、keynote 文章的 iframe: 都用它。

--- 分页(前面留空行),<!-- v --> 纵向子页(缩略图里也各算一页,页码和播放器一致)。日期与文章一样从文件名的 YYYY-MM-DD- 前缀取(front matter 写了 date: 则以它为准),ARCHIVE 与 /slides/ 索引按它排,日期在未来的 deck 也和文章一样先不发布、到期当天上线(系列的 deck 约定与该系列的「系列总结与通关自测」同一天、并排在它之后——date: YYYY-MM-DD 23:30:00 +0800,总结篇是 20:00;系列导航里的「幻灯片」链接也到那天才出现);slides/2026-08-01-reveal-demo.md 是全部语法的活演示;想配长篇文字稿就建一篇 layout: keynote 的文章嵌进去(见本节第 1 小节)。


11. 随笔(Moments)

写不成文章的碎碎念——一张图、一句话、一首歌——放 moments/,导航栏的 Moments。样子是朋友圈式的时间线:左边一条线挂着「日 / 时」,右边是内容;每条下面 ♡(匿名点赞,一个浏览器一票)· 评论(跳到月页底部的评论区)· 链接(这一条的地址 /moments/2026-09.html#20260921-0802)。划线评论、阅读数、Discussion 评论区和文章一样,只是以月为单位。

一个月一个文件,moments/2026-09.md,front matter 一行不用写(目录默认 layout: moments),正文靠日期标题分条,顺序随意,渲染按时间倒序:

## 2026-09-21 08:02 @深圳湾
早起跑了五公里,海边风很大。

![](/img/moments/2026/09/bay-1.webp)
![](/img/moments/2026/09/bay-2.webp)
![](/img/moments/2026/09/bay-3.webp)

## 2026-09-20
> 人生到处知何似,应似飞鸿踏雪泥。
> 泥上偶然留指爪,鸿飞那复计东西。
> —— 苏轼《和子由渑池怀旧》

出差路上想起这首。

## 2026-09-18 23:40
循环了一晚上。

https://music.163.com/#/song?id=347230

只有四条规则,其余就是普通 Markdown:

写法 效果
## YYYY-MM-DD[ HH:MM][ @地点] 一条的开头;时间、地点可省
连续几行 ![](…)(alt 留空) 一组图:1 张大图,2 / 4 张两列,3 张以上三列方格;点开放大,不出「图 N」
> … 最后一行 > —— 作者 引言卡,换行保留(诗),署名右对齐
一行只有一个 URL
  • 网易云 / QQ 音乐 / Spotify / Apple Music 的歌曲链接 → 播放器卡
  • .mp3 / .m4a 直链 → 原生播放器
  • 其他 URL 就是普通链接

能不能播取决于平台版权和读者所在地区(网易云外链在手机浏览器上常是空白,QQ 音乐的好一些;Spotify / Apple Music 国内不通);自托管 mp3 是唯一不依赖别人的路。每张卡片下有「打不开?去原站听」。

懒得开编辑器:tools/moment.py "文字" [--at 地点] [--img a.jpg b.jpg] [--quote "诗句\n第二行" --by 作者] [--music URL] 追加一条到本月文件,图片拷进 img/moments/YYYY/MM/ 并转成 WebP、路径替你写好;--time "2026-09-19 20:00" 补记;不带参数就用 $EDITOR 打开本月文件、预置好当前时间的标题行。

手机也可用 /moments/post.html 发布:应用壳可离线打开,Android 分享暂存 IndexedDB,图片草稿重载后仍在;网络失败会排队按序重试。视频上传前在浏览器用 Mediabunny / WebCodecs 转成 H.264/AAC(短边 ≤ 720),不支持、转换失败或压缩后更大时回退原片;Worker 通过 /media 从 R2 提供视频。

其他:/moments.xml 是随笔单独的 RSS(一条一个 item);ARCHIVE 里每个月一行 [Moments];站内搜索按月页收录。实现见 _plugins/moments.rb。

五、发布前后:质量检查

每次 push 触发 check workflow,做三件事,任何一件失败都会在 Actions 里标红并发邮件:

  1. 构建:jekyll build --strict_front_matter——front matter 写错 YAML 直接失败。
  2. 全站链接检查(lychee 离线模式):每个站内链接、图片、#锚点 必须存在。写错 slug、图片路径、引用了不存在的标题锚点,这里会抓到。
  3. 渲染检查(headless Chrome 跑 tools/check-render.cjs):本次改动的文章逐篇打开,Mermaid 有没有语法错、图片有没有加载失败、代码块有没有溢出。只改了模板 / CSS / JS 时改为抽查最新 15 篇。站外热链的图挂了只告警不判失败。

本地想提前跑:

jekyll serve --future &
~/.claude/skills/browser/scripts/start.cjs          # 起一个带调试端口的 Chrome
node tools/check-render.cjs <slug> [<slug> ...]
lychee --offline --root-dir $PWD/_site --include-fragments --exclude '/tags/?#' '_site/**/*.html'

内容健康报表(不阻塞):npm run audit 列出没 tags、只用过一次的 tag、没副标题、重名、放了半年以上的草稿、没转 WebP 的图、裸 http:// 链接,以及「最近 30 天有实质修改却没写 updated:」的文章(机械性批量提交不算)。隔一阵跑一次,看数字有没有变小。

外链每周一自动检查一次(external links workflow),失效的汇总开成一个带 dead-links 标签的 Issue,不阻塞任何发布。老文章外链腐烂是常态,看到 Issue 有空再修。

审阅 AI 的修订:渲染后的对比 + PR 讨论

AI 改文章不直接进 master,而是走 Code Review 的路子:在独立的 git worktree 上开分支 rev/<主题>、开 PR,每处改动的理由以 PR review comment 的形式挂在改动所在章节的第一处改动行上(tools/review.py <PR#> --notes .review/notes.md 批量发,并列出还没写理由的改动章节)。审阅不看原始 Markdown 的 diff,而是看渲染后的文章:

npm run review -- 45            # PR #45:建 base / head 两份站点,打开 http://localhost:4100/_review/
npm run review -- 4d5c0ef       # 任意一次提交(= 4d5c0ef^..4d5c0ef)或 A..B 区间
npm run review                  # 工作区 vs HEAD(未提交的改动)

页面用站点自己的 CSS、KaTeX、Mermaid 渲染新旧两版,按段落对齐、按词标注(删除红线、新增绿底;代码块按行,表格按行再按格,整段重写时旧新整块并列),未变的段落折叠成「… N 段未变」。顶部可切「修订标注」(单栏)/「左右并排」。改动按 h2/h3 章节分组,每节前一张卡片:PR 上这一节的讨论串(含回复、已解决状态)、没有理由的标「无说明」、下面一个输入框——写下意见点「发评论」就发成 PR 上对应行的 review comment,也能回复已有线程;首页有「批准 / 要求修改」。之后由 AI 读线程、回复「采纳 / 不采纳 + 理由」并推新提交,重跑一次页面就看到更新后的对比;满意就 merge,master 一变就自动部署。

不在 PR 上的改动同样能看渲染对比,只是没有理由与评论那一层。两次 Jekyll 构建约 40 秒,按提交 sha 缓存在 _site-review/(gitignore),再看同一个 PR 只重建变了的一侧。


六、读者侧功能

这些都在文章页上,读者不需要任何配置;作者需要知道的是它们的数据在哪儿。

1. 评论、划线评论、投票、点赞、阅读数

%% 图:评论、划线评论、投票、点赞与阅读数的数据流:读者浏览器经 Cloudflare Worker 与 giscus 到 GitHub,阅读数落到 D1
flowchart TB
    R["读者浏览器"]
    W["Cloudflare Worker<br/>blog-annotations"]
    G["giscus.app<br/>(GitHub 登录中转 + 公开读接口)"]
    GH["GitHub<br/>Discussions / Issues / Reactions"]
    D1["Cloudflare D1<br/>阅读数表"]
    R -- "读讨论串、换 token" --> W --> G --> GH
    R -- "发评论 / 回复 / 投票 / 点赞<br/>(读者自己的 GitHub 身份)" --> GH
    R -- "勾了「同时提交 Issue」" --> W -- "以博客的 GitHub App 身份建 Issue" --> GH
    R -- "阅读数 +1" --> W --> D1
    classDef c fill:#f6f8fa,stroke:#d0d7de,color:#24292f;
    class R,W,G,GH,D1 c;
  • 数据都在 GitHub:每篇文章对应仓库 Discussions 的 Comments 分类里一条以 URL 为标题的讨论串。评论、回复、划线评论都是这个讨论串里的普通评论,去 GitHub 上也能看、能删、能锁。
  • 划线评论:读者选中正文任意文字 → 点浮出的「评论」→ 评论挂在那句话上,正文那句话下面出现琥珀色虚线(多个讨论串时为实线)和计数标记。它在 GitHub 上是一条开头带引用和 § 原文位置 链接的评论。我改了原文措辞,模糊匹配仍能对上;改动太大就列为「未能定位」,评论不会丢。
  • 投票 / 点赞:评论的 ▲ 分数 ▼ 是 GitHub 的 👍/👎 reaction,一人一票由 GitHub 保证;文章的「点赞」是匿名的——Worker 的 D1 里一行计数,不用登录,一个浏览器一票(localStorage 记着),信任级别和阅读数一样。读者还可以对一段话点「点赞」(选中文字后工具条上的按钮),同样匿名:Worker 的 passage_reactions 按段保存 up / share,hash 就是 #annot-<hash> 里那个 FNV-1a,quote 存原文;有点赞、没有评论的段落也能画出下划线。段尾标记合成 💬 n · 👍 n,讨论面板顶部有「点赞」和「分享」按钮。评论区顶部的「最受关注的段落」按 点赞 + 2×评论投票 + 评论数 排名。
  • 作者划重点:作者在文章页登录 GitHub 后,选中文字点工具条上的「划重点」即可预先标出重点;在已标出的 passage 里再选中并点「取消划重点」可撤销。读者看到的就是普通下划线,没有作者标签或特殊颜色;只有划重点、没有评论和点赞时不显示段尾图标,读者点击下划线文字打开面板后即可评论、点赞。若整段 pin 都在链接里,会在链接外留一个评论标记,仍可打开段落面板。需要部署 Worker(wrangler deploy);第一次写反应表时会自动补上 pinned 列,不用手动迁表。作者的 pin 不会出现在「读者划出来的句子」,除非读者后来点了赞。
  • 章节级反应:随笔每条下面的 ♡ 是一个匿名、一个浏览器一票的「章节」点赞(js/annotations.js 的 renderChapterBars 填页面自己放的 .sec-react 占位),存的还是 passage_reactions,只是 quote 写成 § 标题。文章标题末尾曾有过章节点赞按钮,当时留下的章节数据不再显示;随笔的 ♡ 保留。每条 reaction 和每条划线评论都带所在章节(最近的 h2/h3,reaction 存 section 列,评论头写成 · 位于「…」)。
  • 反馈与修正:需要提出改法时,使用普通评论;划线评论的 Issue 关闭、作者回复「已修正」或给评论一个 🎉 后,相关划线变绿。只点赞、没有评论的段落没有「已修正」状态。
  • 反馈方式:工具条保留「评论」,不再提供独立的「建议修改」按钮,避免相似入口增加读者负担。需要提出改法时,直接在普通评论中说明即可;已有历史建议评论仍然保留。
  • 代码块的 header 栏:每个围栏块顶上一条栏(js/figures.js):左「代码块 N:标题」/「文本块 N」,右语言标签、复制、评论——评论按钮和图片上的是同一个:点了就选中标题(没标题选第一行)、弹出普通的划线工具条,省掉拖选几十行的动作;引文是标题或第一行,代码改了旧评论也还在。标题写法见第 8 小节。跑不通的反馈就是一条普通的划线评论 + Issue,环境、报错读者自己写。
  • 同时提交 Issue:读者认为写错了可以勾上这个,会在仓库开一个带 划线评论 标签的 Issue 并署读者名,评论上带红旗徽章——这是我的勘误工作队列,去 Issues 里按标签筛。
  • 已修正:带 Issue 的划线评论以 Issue 关闭为准(提交时写 Fixes #N 即可,前端匿名读 REST 查状态,open 缓存 10 分钟、closed 一天);不带 Issue 的,我在下面回复含「已修正 / 已修复 / 已采纳」的话,或给那条评论一个 🎉。命中后那句话的线变绿色实线,段尾标记带 ✓,评论挂绿色「已修正」徽章,「最受关注的段落」不再算它。
  • 旧数据:过去的存疑计数和原因仍保留在 D1,但页面、仪表盘和修订简报不再显示;Worker 只接受 up / share,新的存疑操作会以 400 拒绝;up > 0 或作者 pin 的无评论段落会作为锚点。
  • 原文已修改:定位不上的划线评论列在评论区顶部,带引文前 24 字和作者——多半是因为那条评论原文才改的,修完记得回复「已修正」。
  • 阅读数:唯一不在 GitHub 的数据,存 Cloudflare D1,一个浏览器一篇文章一天算一次,本地预览不计数。它是量级参考,不是统计产品。
  • 作者的评论只有作者看得见:我自己(arganzheng,仓库 OWNER)在文章里发的评论和划线评论是过程态——读文章时随手记下哪里要改,然后让 AI 按这些评论修订(修订简报 / 待修订 issue 读的就是它们)。它们不是给读者看的:只有我登录后页面才显示(含下面的回复),其他读者看不到,也不算进评论数、段尾 💬 计数、「最受关注的段落」和列表页的评论徽章(Worker GET /stats 同样扣掉)。我回复读者评论的「已修正」等仍是公开的,旁边带「作者」标签。注意这是页面层面的隐藏:评论本身还在公开的 GitHub Discussions 里,去 GitHub 上仍能看到。
  • 通知:有人评论或回复,GitHub 会按我的通知设置发邮件(Discussions 的 watch 要开着)。回复读者直接在页面上或 GitHub 上都行。

Worker 代码在 tools/annotations-worker/,部署用 wrangler deploy;密钥包括建 Issue / 提交随笔用的 GitHub App 私钥,以及可选的 MOMENT_KEY(供随笔发布 API-key 方式使用),都以 Cloudflare secret 形式存放。README 里有从零配置的步骤。部署后可用这条无认证请求确认 pin 路由版本:curl -s -X POST https://blog-annotations.arganzheng.workers.dev/reactions/pin -H 'Origin: https://arganzheng.life' -H 'Content-Type: application/json' -d '{}'。返回「需要登录」说明新路由已部署;Not found 是旧版本;漏掉 Origin 会先得到 403 Origin not allowed。

D1 的备份与恢复。 评论和划线评论都在 GitHub Discussions 里,丢不了;但阅读数、点赞、分享、段落的赞 / 分享(views、views_daily、votes、shares、passage_reactions 五张表;旧存疑数据也仍留在最后一张表中)只存在 Worker 的 D1 数据库 blog-views 里,仓库没有副本。所以有一个 d1 backup workflow(.github/workflows/d1-backup.yml)每周日早上 wrangler d1 export 一次,导出的 SQL 作为 workflow artifact 保留 90 天(滚动约 13 份;仓库是公开的,所以不 commit 进 git,artifact 只有协作者能下载)。它需要两个仓库 secret:CLOUDFLARE_ACCOUNT_ID 和一个权限为 Account · D1 · Edit 的 CLOUDFLARE_API_TOKEN(export 是写类接口,Read 不够)。

# 手动备份(本地,wrangler 已登录即可;也可以 gh workflow run "d1 backup")
cd tools/annotations-worker && wrangler d1 export blog-views --remote --output blog-views.sql

# 恢复:找到最近一次成功的 run,下载 artifact,整份灌回去
gh run list --workflow "d1 backup" --status success --limit 3
gh run download <run-id> -n d1-blog-views-<日期>
cd tools/annotations-worker && wrangler d1 execute blog-views --remote --file blog-views.sql

导出的文件就是 CREATE TABLE + 一行一条的 INSERT(几十 KB),所以恢复到一个新建的空库最省心:wrangler d1 create blog-views 后把新的 database_id 填进 wrangler.toml 再 execute;灌回现有库会因为主键冲突报错,需要先把对应表 DROP 掉。只想找回某一篇的数字,直接在 SQL 文件里搜路径,手工 wrangler d1 execute --command "UPDATE views SET count=… WHERE path='/slug.html'" 也行。

2. 搜索

导航栏放大镜或 /search/ 页面,纯前端、无服务。构建时 _plugins/search_index.rb 把每篇文章切成索引键——英文单词整词、每个汉字和相邻两字——按哈希分成 256 个小文件放在 /search/idx/,正文每篇一个 /search/doc/<slug>.txt。打开搜索层只下载 68 KB 的标题表;每次查询只拉关键词落到的几个索引桶(每个约 8 KB),再拉当前显示的十篇正文做摘要和高亮,往下滚再取下一页。之前是打开就下 15 MB 全文(gzip 后 5.5 MB)。匹配规则没变:英文按整词、中文按子串,多个词都要命中,标题命中排前。三字以上的中文词是用两字组链近似筛的,所以显示「约 N 篇」,真正展示前会用正文复核,不会多出旧版没有的结果。

(试过 Pagefind:它按”词”建索引,中文分词后「参数服务器」会拆成「参数」+「服务器」命中 148 篇,「一致性哈希」反而漏掉一半,摘要高亮成一个个单字——不适合中文子串搜索,放弃。)

快捷键:任何页面按 ⌘K(Windows / Linux 上是 Ctrl+K)或 / 打开搜索层,Esc 关闭。光标在输入框、评论框里时 / 照常输入,不会打开搜索;⌘K 在搜索框里按是全选关键词。

3. 其他

  • 反向链接「LINKED FROM」:文末列出「被 N 篇文章引用」,_plugins/backlinks.rb 构建时扫描所有文章正文里的站内链接(改过 URL 的旧地址也算),按引用文章的日期从新到旧排列。不用手工维护:在 A 里链到 B,B 的文末就自动出现 A。所以写文章时多用站内链接,知识网才连得起来。
  • 链接悬停预览:鼠标停在正文里的站内文章链接上约 0.3 秒,弹出目标文章的标题、日期、所属系列和摘要卡片,不用点开就知道链到哪。数据是构建时生成的 /search/preview.json(约 140 KB,第一次悬停才下载)。只在有鼠标的设备上启用;包着图片的链接、脚注编号、操作栏里的链接不预览;个别链接不想要预览,可以给 <a> 加 data-no-preview。
  • 暗色模式:默认跟随系统(prefers-color-scheme),导航栏的月亮 / 太阳按钮手动切换,选择记在 localStorage["theme"];切回和系统一样的主题时自动恢复「跟随系统」。配色在 less/dark.less。写文章要注意的:透明背景的 SVG 在暗色下会自动垫一张浅色底卡(否则黑字看不清),PNG / WebP 截图不做处理,所以截图最好是不透明的;新加样式时如果写了浅色背景或深色文字,要在 less/dark.less 补一条 html[data-theme="dark"] … 的覆盖,并在暗色下看一眼。组件自己的 display:flex 会盖过浏览器的 [hidden],要再加 &[hidden]{display:none};「生成图片」曾因此在文章分享菜单误显示。
  • 阅读进度与「继续阅读」(js/reading-position.js):文章页顶部一条 2px 细进度条,从正文开头算到版权声明 / 文末推荐之前。读到 5%~95% 之间离开,位置记在本机(localStorage["readpos:<路径>"],记的是最近经过的标题和相对偏移,文章改过也基本能对上);下次打开会在底部提示「上次读到「某节」 · 继续阅读」,点一下滚回去,12 秒或往下滚一屏后自动消失。读过 95% 视为读完、删除记录;记录 90 天过期,最多保留 200 篇。URL 带 #锚点 打开、或已经往下滚了时不提示。页面需有 .post-length 才启用;随笔页不启用。纯本地,不上传任何数据。
  • 章节折叠(js/section-fold.js):文章里最高两级标题(通常是 h2 / h3)左侧有个小三角(桌面端悬停标题出现,触屏常显),点一下收起本节、再点展开;按住 ⌥(Option / Alt)点击,同级的所有章节一起收起或展开。折叠状态不保存,刷新就全部展开。收起的内容用的是 hidden="until-found",所以浏览器的 ⌘F 页内查找、OUTLINE 目录、#锚点 链接、划线评论定位、脚注跳转都能找到并自动展开;打印时全部展开,「复制为公众号格式」导出的也是完整正文。随笔页不启用。
  • 阅读设置:文章页导航栏有一个「Aa」按钮(手机在展开菜单里),三项:字号(小 / 标准 / 大 / 特大)、宽度(默认 / 窄栏,约 760px 一行,适合大屏)、字体(无衬线 / 衬线,用系统自带的宋体,不下载字体)。改了立即生效,记在读者本机(localStorage["reading-settings"]),所有文章通用,「恢复默认」一键还原;_includes/head.html 在样式加载前就应用,不会闪。只影响文章正文:导航、目录、评论区、代码块字体都不变,随笔页不受影响;打印和「复制为公众号格式」始终按默认样式。样式在 less/reading-settings.less。写新样式时如果给正文元素写死了 font-size: 16px 这类像素值,字号设置就管不到它,尽量用 em 或继承。
  • RSS:/feed.xml,最近 10 篇全文;页面 <head> 里有自动发现标签,阅读器直接填域名即可。
  • 文末推荐「YOU MIGHT ALSO LIKE」:_plugins/related_posts.rb 构建时算好。共同 tag 按稀有度加权(quartz 比 AI 值钱得多),排除本系列文章(它们已有上一篇/下一篇和系列目录),别的系列每个最多占一条,只在同一分类(Tech / Life)内推荐;每条后面小字标出依据的 tag。所以每篇都要有 tags,没有 tags 的文章永远不会被推荐。
  • 老文章提示:技术文章写于(或最后更新于)三年以上,正文顶部有一条黄色提示「本文写于 N 年前,部分内容可能已经过时」。年数按构建时间算,每天部署一次所以是活的;随笔不显示;常青文章可写 stale: false 关掉。改完文章写上 updated: 提示就重算。
  • 标题锚点:正文标题 hover 出现 #,点击复制本节链接并滚到该节;触屏常显淡色,公众号导出不带。
  • 标签页 /tags/:顶部标签云和侧栏 HOT TAGS 是同一个 _includes/tag-cloud.html,按篇数分五档字号/字重/颜色(侧栏只显示 > 2 篇的,标签页全部);下面按标签分组列文章。关于 /about/。
  • 归档页 /archive/:文章、幻灯片、随笔(按月)的总表,对应 Notion 的数据库视图。默认还是按年月的时间线;上方可以按类型(Tech / Life / Slides / 随笔)、年份、系列、标签筛选,按发布时间、最近更新或标题排序,并在「时间线 / 表格 / 卡片」三种视图间切换。筛选条件都写在 URL 里(比如 /archive/?tag=AI&sort=updated&view=table),可以直接收藏或链给别人;视图选择记在本机。数据就是页面里构建时生成的条目,不额外请求。系列名取自 _data/series.yml,「最近更新」取 front matter 的 updated:,所以这两个字段写准了,这里的视图才准。 手机上表格只显示日期、标题、更新三列,类型、系列、标签请切到卡片视图看。
  • 无 JavaScript 的归档页:筛选控件默认隐藏,仍会显示构建时输出的静态时间线。
  • 分享卡片:每页都有 Open Graph / Twitter Card / JSON-LD,贴到微信、Twitter、Slack 会显示标题、摘要、图(默认 img/home-bg.jpg,文章可用 header-img 覆盖)。
  • 统计:Google Analytics、百度统计、Cloudflare Web Analytics 三个都接着(_includes/analytics.html、head.html)。
  • 404 页、robots.txt、sitemap.xml(jekyll-sitemap 插件生成)都有。

4. 操作栏(点赞 / 分享)与「复制为公众号格式」「导出」

文章页 COMMENTS 标题上方操作栏(_includes/post-actions.html + js/share.js):「♥ 点赞 N」「分享」;作者登录后还会看到「复制为公众号格式」「导出」和「在 GitHub 编辑」。「点赞」是匿名计数——Worker 的 D1 里一行(GET/POST /votes),不用登录,一个浏览器一票(localStorage 记着),信任级别和阅读数一样;没有点踩。数字显示在文章头部 meta 行末尾的 .post-stats 文字条:阅读、点赞、评论、分享四组文字和数字,由 share.js 统一绘制——点赞 / 分享来自 GET /votes,阅读 / 评论由 annotations.js 通过 blog:stats 事件递过来;首页和 /life/ 列表每篇的 meta 里是同一条文字条,由 share.js 一次请求 GET /stats?paths=… 填(每篇边缘缓存 2 分钟)。分享数:分享菜单里任何一项真正用过(系统分享完成、打开微博 / X / LinkedIn、显示微信二维码、复制链接成功)就 POST /shares 加一,信任级别同阅读数。列表页没有任何动作按钮——读者在列表页点赞、评论、分享的可能性不大。

「分享」点开一个小菜单:系统分享(Web Share API,不支持的浏览器不显示)、微博、X、LinkedIn(官方分享 URL,不加载任何第三方脚本)、微信扫一扫(本地生成二维码,js/vendor/qrcode.min.js 点开才加载)、复制链接。知乎 / 掘金没有分享入口 URL,只能复制链接。

从选区工具栏或段落面板分享划线时,菜单还会有「生成图片」:卡片突出完整引文、淡化同段上下文,页脚放作者头像 / 名字、文章标题和指向这段原文的二维码。卡片正文最多 10 行,超出后以省略号收尾;标题最多两行,仍放不下时第二行以省略号收尾。文章级的「分享」菜单没有这一项(作者决定的:整篇文章没有合适的图片交互)。生成在浏览器里完成(js/passage-card.js),不经过 Worker;卡片固定浅色底,暗色模式下生成的也一样。

作者专用:用 GitHub 登录评论区后(login 等于 _config.yml 的 github_username),操作栏右侧多出一个绿色的「复制为公众号格式」按钮(js/wechat-export.js,点了才加载)。它把渲染好的正文转成一段自包含、全内联样式的 HTML 放进剪贴板,到公众号编辑器或知乎编辑器里直接 ⌘V——这是 mdnice / 墨滴那类工具的原理,只是不用再把 Markdown 贴过去一遍。转换时做的事:

  • 去掉系列导航 / 目录 / 评论 / 高亮 / 复制按钮等非正文内容,删掉所有 class / id,按标签写 style=;代码块的配色从页面实时取(Rouge token → <span style="color:…">)。
  • 外链变脚注:公众号会剥掉非公众号链接,所以链接变成「文字[n]」,文末生成「参考与脚注」列表;Markdown 脚注和名词 tip 也并进这个列表。
  • 公式:取 KaTeX 里的 TeX 源,换成 codecogs 渲染的图片(外部服务,只在你点复制时用到)。
  • 图:站内 .webp 转成 JPEG(公众号素材库不收 webp)、Mermaid 用 htmlLabels: false 重渲染一遍再转 PNG(带 HTML 标签的 SVG 会污染 canvas),都以 data URL 内嵌;其他图片补成绝对地址。一篇二十张截图的文章约 3 MB。
  • 末尾自动加一行「原文:URL」。

粘贴后要自己看一眼的:base64 图片和 codecogs 公式图是否被编辑器成功抓取上传(公众号的行为没有文档,实测为准),代码块配色有没有保留,标题得在编辑器里另填。划线评论、脚注浮窗、目录这些交互必然丢失,所以才有那行原文链接。

作者专用的「修改记录」:你用 GitHub 登录后,文章头部 meta 行的日期后面(有 updated: 的就在「更新于」后面)会多出一个「修改记录」链接,指向 GitHub 上这篇文章源文件的 commits 页,对应 Notion 的 Page history,方便查这篇改过什么、什么时候改的。读者看不到这个链接(和「复制为公众号格式」一样由 js/share.js 按登录身份显示)。链接在 _includes/post-meta.html 里按 page.path 拼出来,不用手工维护。GitHub 的 commits 页不跟踪改名,改名之前的历史要在 GitHub 上点「Follow renames」或本地 git log --follow 看。keynote 布局的元信息位于 iframe fallback 内容里,因此没有可见的「修改记录」。

作者专用的「导出」菜单:同样只在你用 GitHub 登录后才出现,读者看不到入口,网站上也没有任何公开的导出文件(所以没做 llms.txt)。菜单里有两项:

  • 「存为 PDF」:临时切到浅色、套上一份只给导出用的打印样式(去掉导航、目录、操作栏、评论、系列导航、划线标记等,代码块自动换行、图表不跨页),纸张为 A4(@page author-export),然后调起浏览器打印,选「存储为 PDF」即可。折叠的章节会先全部展开。读者自己按 ⌘P 打印仍是浏览器默认效果,不受这份样式影响。样式在 less/print-export.less,全部挂在 html.print-export 下。
  • 「Markdown 源文」:通过 GitHub API 取这篇文章在 master 上的源文件(不是渲染后的页面),按源文件名下载 .md 文件,内容原样保留,包括 front matter 和相对链接;不加 BOM、不改写路径。拿的是已推送的版本,本地没推的改动不会出现在里面。

七、开发与维护

  • 样式与脚本都是构建产物(2026-09-15 起):样式源只在 less/,入口 less/argan-blog.less,npm run css 生成 css/argan-blog{,.min}.css;文章页的十来个脚本由 npm run js 合并压缩成 js/blog.min.js。不要手改这三个产物文件——npm run check 会比对它们和源码是否一致,不一致就拦下 push。此前两个 css 里有几百行手工追加、没有 Less 源的规则,两份 css 之间还互相漂移过(9-14 的字体调整只进了未压缩那份,线上一直没生效),现在都收进 less/theme-overrides.less 和 less/dashboard.less 了。
  • 推送前检查:npm run check(= tools/check.sh,npm run hooks 一次后作为 pre-push 钩子自动跑):代码块里没包 raw 的 {{、产物与源码一致、jekyll build 真的跑完、Font Awesome 子集、站内链接、空白符。约 20 秒;一篇有问题的草稿会让整个 build 挂掉、_site 悄悄停在旧版本,这道检查就是为了在本地而不是 Actions 里发现它。
  • 字体:不引入网络字体,全部走系统字体栈:-apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", … , sans-serif——Mac / iOS 是 San Francisco + 苹方,Windows 是 微软雅黑,Android 是 Roboto + 思源。零下载、零闪烁,中英文混排是各平台自己调好的。代码块用 SF Mono / Menlo / Consolas 一路兜底。别加 Google Fonts。
  • 前端依赖:2026-09 起页面不再加载 jQuery 和 Bootstrap 的 JS(也去掉了 FastClick)——主题剩下的几十行(表格 / 视频响应式包裹、导航栏随滚动隐现、浮动目录固定)在 js/argan-blog.js 里用原生 DOM 重写,导航栏折叠一直是 nav.html 里的原生代码。新功能一律原生 JS,不要再引 jQuery。Bootstrap 3 的 CSS 保留(栅格、navbar、表格等还在用),也不要直接换 Bootstrap 5——那是一次 UI 重构,不是升级。
  • 响应式:Bootstrap 3 栅格。≥ 1200px 文章页右侧有浮动目录、首页右侧有侧栏(头像、一句话、HOT TAGS);< 992px 侧栏挪到页面底部、目录收起为按钮;代码块和表格在窄屏横向滚动而不是换行。
  • Font Awesome:用的是自托管子集(20 KB,全量 77 KB),里面固定包含约 150 个常用图标,正常写文章不用管。如果用了子集外的图标,check workflow 会失败并直接列出图标名,这时跑 python3 tools/fa-subset.py(需要 pip install fonttools brotli)或把图标名加进脚本的 ALWAYS 列表。
  • 依赖:Ruby 依赖在 Gemfile / Gemfile.lock(改动后跑 bundle lock --add-platform x86_64-linux,CI 是 Linux);Dependabot 会自动开 PR 升级。Node 只在本地用(npm install 装 less、clean-css、uglify-js 三个构建工具,npm run css|js|check)。
  • Service Worker:sw.js 保留但已禁用(service-worker: false),不要开——它的实现会给每个请求加随机参数,等于关掉所有缓存。
  • 主题来源:基于一个 2017 年的 Jekyll 主题,之后自己长期改造,两边早已分叉,不要尝试整体合并任何主题更新;想要的功能手动移植。
  • 评论系统的两个外部依赖:giscus.app 的 OAuth / 读接口(稳定但非公开契约)和 Cloudflare Worker 免费额度。任何一个挂了,页面退化为「复制评论内容去 GitHub 粘贴」,文章本身不受影响。
  • 同时开着几个 PR 时的冲突:每个改样式或脚本的 PR 都会改 css/argan-blog.min.css、js/blog.min.js(.map) 这几个单行产物,合掉其中一个,其余的几乎一定冲突,而且没法手工合并。处理办法固定:在分支上 git merge origin/master,冲突的产物文件随便保留一边,然后 npm run build 从源码重新生成,npm run check 通过后再提交推送。只要 less/ / js/ 源文件本身没冲突,就不用看产物的 diff。回滚通过 merge commit 引入的功能时,用 git revert -m 1 <merge sha> 撤销该合并。

八、FAQ

push 了但线上没变? 先看 Actions 里 deploy 是否绿;绿了就是缓存——GitHub Pages 所有文件 max-age=600,按 F5 刷新即可,最坏等 10 分钟。

构建失败,日志里有 Liquid Exception? 九成是代码块里的 {% / {{ 没包 raw(第四节第 9 小节),或 front matter 的 YAML 写错(冒号后没空格、引号没闭合)。

check 红了,deploy 绿了,要紧吗? 站点已经上线了,check 是质量检查:看它红在哪一步——链接检查(写错了 slug / 图片路径 / 锚点)、渲染检查(Mermaid 语法、图片)、图标子集。修了再 push 一次即可。

Mermaid 图不显示或报错? 保留字(end、call、click、style、class、default、o、x)不能当节点 ID;标签里的 [ ] { } 要写成 #91; #93; #123; #125;;sequenceDiagram 一行一条消息。横向布局在 755px 宽的正文里会缩到看不清,用 flowchart TB。

公式不渲染? 行内和块级都用 $$…$$(kramdown 的 mathjax 引擎语法,前端换成 KaTeX 渲染)。代码块里的 $ 不会被误伤。

想临时下线一篇文章? front matter 加 published: false,不用移文件。

想改一篇文章的 URL? 尽量不要——评论串按 URL 对应。非改不可时新文件加 redirect_from: /old.html,旧评论会留在旧讨论串里。

评论区一直「正在加载」? 打开浏览器控制台看 [annotations] 日志:Worker 挂了(Cloudflare 状态页)或 giscus.app 抽风。都不影响文章阅读。

读者说图标是空方块? 用了图标子集之外的图标,跑 python3 tools/fa-subset.py 重新生成(第七节)。

本地 jekyll serve 看得到、线上看不到某篇? 文章日期在未来。线上每天 00:05 会自动构建一次把到期的放出来。

看不到「划重点」按钮? 先确认是在文章页用作者 GitHub 账号登录;非作者不会显示按钮。按钮可见但点击提示「worker 还没部署新版本」时,在 tools/annotations-worker/ 运行 wrangler deploy;提示登录过期则重新登录 GitHub。

看不到导航锁图标? 在任一文章页用 GitHub 登录;作者状态按浏览器缓存在 localStorage.blogAuthor。登出或 session 过期 / 验证失败会清缓存,再登录即可。

看不到「导出」或「修改记录」? 两者都是作者专用;在文章页用 GitHub 登录后才显示。

Worker curl 返回 Origin not allowed? 请求要带站点 Origin,不能删 -H 'Origin: https://arganzheng.life';用上方 pin 路由探测命令核对新版本。


九、速查

常用操作速查
我想… 做法
发一篇文章 _posts/日期-slug.md 写好 → push → 3 分钟后上线(未来日期则到那天凌晨)
大改一篇旧文 front matter 加 updated: 日期
发私有文章 front matter 加 visibility: private;作者从导航锁图标进 /private/
加进某个系列 front matter 加 series: <key>;新系列先改 _data/series.yml
预先划重点 文章页用作者 GitHub 账号登录 → 选中文字 →「划重点」(Worker 改动后 wrangler deploy)
导出文章 作者登录后点「导出」→「存为 PDF」或「Markdown 源文」
放图 img/in-post/,大图跑 tools/webp-images.py --apply
本地预览 jekyll serve --future
筛选归档 /archive/?tag=AI&sort=updated&view=table(条件在 URL,可分享)
部署并验证 Worker cd tools/annotations-worker && wrangler deploy;验证:curl -s -X POST https://blog-annotations.arganzheng.workers.dev/reactions/pin -H 'Origin: https://arganzheng.life' -H 'Content-Type: application/json' -d '{}'(应返回「需要登录」)
发布前自检 node tools/check-render.cjs <slug>;或直接 push 看 check
看谁评论了 GitHub → Discussions → Comments 分类;邮件通知
看读者报的错 GitHub → Issues → 标签 划线评论
看死链 Issues → 标签 dead-links(每周一更新)
部署失败 Actions → deploy → 看红色那步;多半是 front matter YAML 或 Liquid 语法
图标不显示 看 check 的 Font Awesome 那步给出的名字,跑 tools/fa-subset.py

十、Releases

2026 年 8 月起大改,以下按自己的节奏记版本;每发一个功能就在这里加一条。

V2.70 — 2026-10-11:作者专用文章入口(导航锁图标 + /private/)

  • 作者登录后可从导航锁图标进入 /private/ 查看私有文章;普通读者看不到入口和正文。

V2.69 — 2026-10-11:作者专用文章(visibility: private)

  • 作者专用文章仍照常构建,但从读者浏览与搜索中隐藏、标记 noindex,并在页面上验证 GitHub 登录;源文件和 HTML 仍公开,不用于存放秘密。

V2.68 — 2026-10-11:「分享」菜单与划线卡片标题优化

  • 文章级「分享」菜单不再提供「生成图片」;划线卡片的文章标题最多换成两行,过长时在第二行以省略号收尾。

V2.67 — 2026-10-11:划线分享生成图片

  • 选区工具栏和段落面板的分享菜单都可生成微信式卡片:引文用大号衬线字突出,同段上下文渐淡,页脚带作者资料与原文二维码;二维码沿用 passage 定位链接。

V2.66 — 2026-10-11:「导出」下拉菜单

  • 作者专用的「存为 PDF」与「Markdown 源文」合并到操作栏的「导出」下拉菜单;Markdown 源文会按原文件名下载 GitHub 上的源文件,内容原样保留(包括 front matter),不加 BOM、不改写内容;复制为公众号格式和 GitHub 编辑入口仍各自独立。

V2.65 — 2026-10-10:作者划重点

  • 作者在文章页登录 GitHub 后选中文字点「划重点」,也可在已划重点的段落中选择文字并点「取消划重点」。读者看到普通下划线;只有划重点、没有评论和点赞时不显示段尾图标,点击下划线文字即可打开面板并追加评论、点赞。部署 Worker 需运行 wrangler deploy;作者 pin 不进入「读者划出来的句子」,除非读者后来点赞。

V2.64 — 2026-10-10:去掉「存疑」

  • 读者工具条与段落面板不再提供「存疑」;已有存疑数据仍留在 D1,但不再显示。部署 Worker 需要运行 wrangler deploy。每周「待修订」Action 会把唯一反馈信号是存疑的旧队列 Issue 关闭。

V2.63 — 2026-10-10:归档页的筛选、排序与表格 / 卡片视图

  • /archive/ 加了类型 / 年份 / 系列 / 标签筛选、按发布或更新时间排序,以及表格、卡片两种视图,条件写在 URL 里可直接分享。见第六节第 3 小节。

V2.62 — 2026-10-10:作者专用的 PDF 与 Markdown 导出

  • 新增「存为 PDF」和「复制 Markdown 源文」;后来合并进「导出」下拉菜单(见 V2.66)。打印样式只在点菜单项时生效,读者打印不受影响;Markdown 直接取 GitHub 上的源文件。见第六节第 4 小节。

V2.61 — 2026-10-10:阅读设置(字号 / 宽度 / 字体)

  • 对照 Notion 的 Small text / Full width / Font:文章页导航栏「Aa」按钮调字号、窄栏、衬线字体,记在读者本机、全站文章通用。见第六节第 3 小节。

V2.60 — 2026-10-10:作者专用的「修改记录」链接

  • 作者登录后,文章头部的日期后面出现「修改记录」,直接跳到 GitHub 上该文件的 commits 页,对应 Notion 的 Page history;读者看不到。见第六节第 4 小节。

V2.59 — 2026-10-10:正文章节可折叠

  • 对照 Notion 的 toggle heading:文章最高两级标题左侧加折叠三角,⌥-点击批量折叠同级章节。收起的内容用 hidden="until-found",页内查找、目录、锚点、划线评论、脚注跳转都会先展开目标;打印和公众号导出始终是完整正文。折叠范围在点击时现算,后插入的表题、代码块工具栏等节点也会一起收起。用法见第六节第 3 小节。

V2.58 — 2026-10-10:GitHub 风格提示块

  • > [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] 构建时渲染成带图标的彩色提示框,可在同一行自定义标题,暗色模式单独配色。写法见第四节第 6 小节末。

V2.57 — 2026-10-10:阅读进度条与「继续阅读」

  • 文章页顶部细进度条;长文读到一半离开,回来时底部提示「上次读到「某节」 · 继续阅读」。位置只存在读者本机。见第六节第 3 小节。

V2.56 — 2026-10-10:暗色模式

  • 全站暗色主题,默认跟随系统,导航栏按钮手动切换。透明 SVG 插图垫浅色底卡、代码行号栏、弹层、评论区都有暗色适配;顺手修了手机菜单被限高 340px、底部两项露在背景外的问题。

V2.55 — 2026-10-10:反向链接、链接悬停预览、搜索快捷键

  • 文末「LINKED FROM」列出引用本文的文章(首次构建统计到约 1000 条站内引用);站内链接悬停弹出目标文章摘要卡片;⌘K / Ctrl+K / / 打开搜索。见第六节第 2、3 小节。

V2.54 — 2026-10-01:/slides/ 列表在平板上不再被劈成两列

  • 系列总览文末的 deck 卡片用的 .deck-card 是全站样式(缩略图 文字 两列网格),而 /slides/ 索引的卡片恰好也叫 .deck-card,于是每张卡片在平板宽度下被劈成「封面一列 + 竖排标题一列」。索引卡片改名 .deck-tile,恢复封面在上、标题在下;封面上的标题给年份徽章让出位置、过长时截成三行。

V2.53 — 2026-10-01:幻灯片遵守发布日期,系列 deck 与总结篇同日

  • deck 是页面,Jekyll 不会像文章那样把未来日期的页面压着,29 份系列幻灯片都按写作日(9-28 ~ 10-05)标了日期,结果系列正文还没发完、deck 先出现在 /slides/ 和 ARCHIVE 里。_plugins/slides_date.rb 现在对未来日期的 deck 做与文章相同的处理:不带 --future 的构建(线上部署)不输出它,到期由每日重建上线;同时从 site.data.series 里摘掉该系列的 slides:,系列导航、总览页的 deck 卡片、/series/ 树都不会链到一个还不存在的页面。
  • 29 份系列 deck 的文件名前缀与 date: 改成该系列「系列总结与通关自测」那篇的日期(URL 不变),时间写 23:30:00 +0800,在 ARCHIVE 里排在总结篇(20:00)之后、作为该系列最后一项:读完总结当天拿到 deck;8 个还在连载中的系列(模型作为组件 … ML 编译器)的 deck 随之推迟到各自的总结日。

V2.52 — 2026-10-01:问答脚注覆盖全文的引导问题,标签改为「本文引导问题答案」

  • 问答脚注原来只给文首的问题作答,但文章中间(章节导入句、总纲分章导读、路线图逐层说明、小结回看)抛出的问题没有答案。现在凡是文章抛给读者、随后由正文回答的引导问题都挂 [^qN],按阅读顺序编号,文末逐条作答;总纲与路线图也补上了问答脚注。标题问句、自测题、回顾篇里紧跟「结论」的「核心问题」、表格 / 代码里的问号不算。
  • 文末脚注列表的标签「文首问题的答案」改为「本文引导问题答案」(less/extras.less);新增 tools/qfootnotes.py(--check 校验引用 / 定义一一对应、非空、按序;--fix 重排编号),npm run check 顺带跑它。

V2.51 — 2026-09-30:文本块也带行号,代码块 header 调浅

  • ```text 与没写语言的块也按代码块的规则带行号(≥ 2 行;{:.lineno} / {:.no-lineno} 照旧可单块开关,ASCII 图等靠左边缘对齐的用后者关掉)。
  • 代码块 / 文本块整体改成一张浅卡片:细边框、header 用与代码区接近的浅色加一条细线(原来是一条偏深的灰栏),标题用中灰而不是近黑,语言标签改成小胶囊。

V2.50 — 2026-09-30:作者自己的评论只对作者显示

  • 我(arganzheng)发的评论 / 划线评论是给 AI 修订用的过程记录,不是给读者看的:只有我登录时页面才渲染它们(含其下回复),其他读者看不到,评论数、段尾计数、「最受关注的段落」、列表页评论徽章(Worker /stats)都不计入。我在读者评论下的回复照旧公开。评论仍在公开的 GitHub Discussions 里,只是页面不显示。

V2.49 — 2026-09-30:去掉标题末尾的「点赞 / 没看懂」

  • 每个 h2–h6 末尾的两个小按钮(V2.16 加的)用起来用处不大,去掉;表题、图题、代码块标题已经是更精确的反馈锚点。随笔每条的 ♡ 不受影响,旧的章节数据仍在仪表盘和简报里。

V2.48 — 2026-09-30:代码块的 header 栏与 title="…"

  • 围栏块的复制 / 评论按钮从右上角悬浮改成块顶一条常显的 header 栏(VitePress 风格):左「代码块 N:标题」,没写语言或 ```text 的叫「文本块 N」(两套编号),右语言标签 + 复制 + 评论;触屏不再需要 hover。标题写在围栏那行 ```cpp title="…"(_plugins/code_titles.rb 先转成 kramdown 的 {: data-title}),和图题、表题一样是这个块的划线锚点;没标题的块评论按钮选中第一行,不再是整段代码,改代码不再把旧评论变成「未定位」。微信导出把有标题的栏变成块上一行小字。写法见第三章第 8 小节。

V2.47 — 2026-09-30:匿名存疑的「已修正」与「清除」

  • 以前存疑只有读者本人能取消,作者改完那段还是红问号。现在段落面板的反应行里,作者(GitHub 登录且是仓库 owner)多出「标记已修正 / 清除存疑」:已修正保留计数、段落变绿 ✓(悬停「作者已修正(原 N 人存疑)」),之后再有人存疑只显示新增的数并重新变红——这就是修改没改到位的信号;清除把计数和原因清零,不可恢复。仪表盘「读者划出来的句子」每行也有这两个按钮(已修正的行变绿、排后面,可撤销),修订简报和「待修订的文章」只算未处理的存疑。Worker 新增 POST /reactions/resolve,用 giscus 的 GitHub token 到 GET /user 核对是不是 owner,不加新密钥。

V2.46 — 2026-09-21:随笔(Moments)——朋友圈式的碎碎念时间线

  • 新栏目 /moments/:一个月一个 Markdown 文件,## 日期 时间 @地点 分条,图片自动排成九宫格、引言变卡片、一行音乐链接变播放器(网易云 / QQ 音乐 / Spotify / Apple Music / mp3)。每条可 ♡(匿名,复用章节点赞)、可划线评论,阅读数和评论区按月记。tools/moment.py 一条命令追加;独立 RSS /moments.xml;进 ARCHIVE 和站内搜索。见第四章第 11 节。

V2.45 — 2026-09-21:存疑原因可多选

  • 讨论面板里「哪里不对?」的五个原因从单选改成多选:每个 chip 独立开关,各自向 Worker 发一次 kind: 'reason'(加计 / prev === reason 减计),Worker 不用改;取消存疑时把自己选过的全部减回去。本机记的是逗号连接的 key(旧的单值照常可读)。

V2.44 — 2026-09-22:幻灯片落地页——网页版 PowerPoint 式的缩略图 + 播放器,评论与统计

  • /slides/xxx.html 不再是全屏 deck,而是落地页:左栏每页一张缩略图(当前页高亮、点击跳页),右边 16:9 播放器 + 翻页 / 页码 / PDF / 全屏工具栏,#/N 记当前页;文章式头部带阅读 / 点赞 / 评论数,底部是操作栏和评论区。全屏演示版移到 /slides/xxx/play.html(_plugins/slides_deck.rb 自动从同一个文件生成),keynote 文章和 PDF 导出改用它——keynote 仍是「全屏幻灯片 + 讲稿」,落地页是「缩略图 + 播放器 + 评论」,两者从此有区分。/slides/ 索引去掉弹窗,卡片直接进落地页,显示页数与阅读 / 点赞数。见第 10 节。

V2.43 — 2026-09-22:代码引用——正文说明与代码行绑定

  • 代码里独占一行的 # !ref 名字(+N 多包几行)在渲染时被删掉,下一行成为锚点;正文用普通链接 [文字](#名字) 引用。悬停正文引用对应行亮起、点击滚过去;代码左侧蓝色的行号悬停 / 轻触就地弹出那段说明(复用脚注气泡)。没有 JS 是普通锚点跳转;公众号导出把引用文字后补「(第 N 行)」。写法与示例见第 7 节末。参考 Code Hike 的 code mentions:说明留在正文里(可划线、可搜索、可导出),代码保持干净。

V2.42 — 2026-09-21:代码块行号

  • 标了语言、≥ 2 行的代码块左侧有行号(_plugins/code_lines.rb 构建时把每行包成 span.line,行号是 CSS 计数器),横向滚动时行号钉在左边;```text 与没写语言的块(多为输出、日志、ASCII 图)不带,{:.lineno} / {:.no-lineno} 按块覆盖。文中「第 N 行」从此有的可指;复制按钮、划线引用、搜索索引、公众号导出都不含行号。

V2.41 — 2026-09-21:Infra 地图的选修《ML 编译器内部》写齐

  • 十三篇正文 + 总纲 + 总结(11-21 … 12-04):编译器骨架、LLVM、MLIR 两篇、Triton 编译器七篇(前端、AxisInfo、layout 与 Linear Layout、layout 优化、软件流水 / Hopper / Blackwell / Gluon、下降到 LLVM、缓存与运行时与 AMD 对照)、TVM 对照、开发者工作台。版本基线 Triton v3.8.0、LLVM 23.1.1、TVM v0.26.0。
  • 全部 IR 在一台没有 NVIDIA GPU 的 Mac 上跑出来:Homebrew LLVM 的 mlir-opt / opt / llc、macOS 从源码构建的 Triton(triton-opt、lit、假 ptxas 编到 PTX、AMD gfx942 编到 ISA)、从源码构建的 TVM(Metal 上真跑)。Infra 地图选修行、依赖图、系列总览表与全栈地图同步;构建方法记在 AGENTS.md。

V2.40 — 2026-09-17:AI 修订走 PR,本地看渲染后的对比

  • npm run review -- <PR#|提交|A..B>:Jekyll 各建一份新旧站点,按段落对齐、按词标注新旧两版渲染后的文章(不是 Markdown 源码),单栏修订标注 / 左右并排可切,未变段落折叠。
  • 改动按章节分组;AI 用 --notes 把每处修改的理由发成 PR 上对应行的 review comment,页面把线程贴到章节旁、缺理由的标「无说明」,意见直接从页面发到 PR,回复、批准、要求修改都在这里;合并即上线。见第五节。

V2.39 — 2026-09-16:划线评论的几处交互

  • 选中的文字落在自己评论过的那段里时,工具栏的「评论」变成「编辑评论」,点开直接进入那条评论的编辑;落在别人评论过的段落里则是「加入讨论」。此前一律默认新增评论,想改自己的评论要多点两下。
  • 面板里发表、回复、保存编辑成功后都自动收起(划线闪一下 + 右下角提示确认);文末评论区不变。
  • 带有未关闭 GitHub Issue 的划线用红色实线加淡红底,段末标记带 ⚑,与「存疑」的红色点线区分开。
  • 公式本身也能划线了:选中(或选到一半)一个 KaTeX 公式就整个算作一段,引用的是它的 TeX 源码(如 s = 8192),划线、标记、存疑红线都套在公式框上;脚注正文和单个字也能划。
  • 工具栏「搜一搜」变成三选一:站内搜索(直接打开站内搜索层并带入选中文字)、Google、Google AI 模式(udm=50,把选中的话包进一段提示词——出自哪篇文章的哪一节、请解释原理并指出疑点;URL 没有 system prompt,只能这样带)。
  • 图片 / 图表不再点击放大、没有 zoom 光标;放大进右上角按钮条(复制 · 放大 · 评论),按钮条常显(GitHub 风格)而不是悬停才出现——之前在图题上划线,松手那一下会把大图弹出来盖住工具栏。

V2.38 — 2026-09-16:应用地图七个系列写齐

  • L2《Prompt 与上下文工程》(10-05 … 10-11,六篇)、L3《检索与知识接入》(10-12 … 10-19,七篇)、L4《工具、Agent 与 harness》(10-20 … 10-29,九篇,含 Codex / DeepSeek Harness / Claude Code / OpenHarness 的十二维源码级对照与 Agents API / SDK / 自托管三种交付形态)、L5《评测、可观测与可追溯》(10-30 … 11-06,七篇)、L6《生产化与运营》(11-07 … 11-14,七篇)、L7《产品与体验》(11-15 … 11-20,五篇),每个系列末尾一篇总结自测;与 L1 一起共 47 篇正文 + 7 总纲 + 7 总结。
  • 应用地图七层各加系列指针,「已有的文章与系列」表七行齐;全栈地图的进度改为「七层齐」。写法与事实基线记在 AGENTS.md「Writing AI-Infra series posts」的应用系列段。

V2.37 — 2026-09-16:搜索不再下载全文

  • 全文索引改为构建时生成的分桶倒排索引(_plugins/search_index.rb),打开搜索层从下载 15 MB 变成 68 KB,一次查询几十到几百 KB;结果、排序、摘要与旧版逐条一致(27 个中英文查询对照)。结果按 10 篇分页,滚到底自动加载。
  • 删掉 search.json / search-content.ndjson;npm run check 新增索引完整性检查。

V2.36 — 2026-09-16:应用地图开始写系列,第一个是 L1《模型作为组件》

  • 应用地图的七层按 L1 → L7 各写一个系列(不做 labs,每篇末尾是「实践建议」,案例取业界公开事件与开源项目)。L1《模型作为组件:契约、失效模式与选型》总纲 09-28、六篇正文 09-29 … 10-04、总结自测同日:失效模式(非确定性的 batch 不变性、幻觉的法律代价、上下文标称 ≠ 有效、越界)、API 契约两篇(四家共同骨架;推理模型的 thinking / effort / 跨轮状态)、成本与延迟的账(四家 2026-09 价目、缓存收支平衡、多轮二次增长)、选型(Leaderboard Illusion、自己的评测集、弃用周期)、客户端工程。
  • 应用地图新增「已有的文章与系列」表,L1 段加系列指向;全栈地图三处「系列待写」改为进度。

V2.35 — 2026-09-16:Font Awesome 4.7 → 7

  • 子集工具改为从 npm 包 @fortawesome/fontawesome-free 的元数据生成,三个字体文件共 24 KB;标记约定 fa fa-x(实心)/ fa fa-regular fa-x(线框)/ fa fa-brands fa-x(品牌),老的 -o 名字不再可用,tools/fa-subset.py 会指出对应的新名字。
  • npm run check 和 npm run serve 改用 bundle exec,避免裸 jekyll 拿到 Gemfile.lock 之外的 gem 版本。

V2.34 — 2026-09-16:依赖升级

  • Mermaid 10.9.1 → 11.17.2、KaTeX 0.16.11 → 0.18.7(144 篇含图、171 篇含公式的文章逐篇在真实浏览器里过了一遍,0 个渲染错误)。
  • gems 更新到 Jekyll 4.4.1 允许的最高版本;CI 的 Ruby 从 3.3 对齐到本地的 4.0。
  • Font Awesome 单独做,见 V2.35。

V2.33 — 2026-09-16:二十一个系列各加一篇「系列总结与通关自测」

  • 每个系列的最后一篇成员(与末篇同一天,front matter date: … 20:00:00 排在其后,URL /<key>-series-recap-and-self-test.html):一张「问题 → 结论 → 必记数字」总表、逐篇回顾(核心问题 / 结论 / 必记 / 常见误解)、贯穿全系列的几条线、常见误区表,再是三段式通关自测——十道判断与计算、五道跨篇综合、七道面试题(答案要点 + 追问方向 + 好答案与一般答案的区别),答案全部折叠,末尾给「读过 / 掌握 / 能教人」判据与通关标准。
  • 末篇里原来的「系列总结」小节整段删掉(本文小结保留),总纲加导读与目录行;地图上的篇数与时长仍只计正文。写法规范见 AGENTS.md「Writing AI-Infra series posts」。

V2.32 — 2026-09-16:Lighthouse 体检后的收尾

  • /tags/ 页从 1.2 MB 压到几百 KB:每个条目里被注释掉的死 HTML 和缩进占了一半以上。
  • 去掉主题自带的 AnchorJS(cdnjs 加载的 2014 年脚本):和上周加的 .heading-anchor 重复,每个标题曾有两个 #。
  • syntax.css / Font Awesome 改为异步加载,search.js 改 defer;Mermaid / KaTeX / reveal.js 的 CDN 地址写成显式 https://。
  • 无障碍:灰色文字统一加深到 4.5:1 以上(@gray #6a6a6a、GitHub 灰 #59636e),品牌青 #0085a1 → #00788f(肉眼难分,过 4.5 线),正文包进 <main>,图标链接补 aria-label。Lighthouse 无障碍 81–87 → 98–100,对比度 0 项不通过。

V2.31 — 2026-09-16:老文章提示、值得翻新榜、内容健康报表

  • /tags/ 标签云和侧栏 HOT TAGS 统一为同一个 include(按篇数分档的文字云),去掉 tagcloud.js。
  • 三年以上的技术文章顶部提示「本文写于 N 年前」(_includes/post-stale.html,stale: false 可关)。
  • 仪表盘新增「值得翻新的老文章」:阅读 × log(年龄),带简报和编辑入口。
  • 修订简报上方新增「待修订的文章」:有存疑 / 没看懂 / 报错 Issue / 待修订 Issue 的文章按反馈量排成一列,不用再从下拉框里找。
  • npm run audit(tools/audit.py):tags / 副标题 / 草稿 / 图片 / 裸 http / 缺 updated: 的软问题报表。

V2.30 — 2026-09-16:相关推荐重写、补齐 tags、标题锚点

  • 文末推荐从「任一 tag 相同、取最新三篇」(Quartz 的文章推荐的是面试手撕代码)改为 _plugins/related_posts.rb:tag 稀有度加权、排除本系列、每个别的系列最多一条、同分类,并标出依据。
  • 99 篇没有 tags 的老文章全部补上(新增词:运维、编码、序列化、Web、Protobuf、Thrift、开放平台、搜索、大数据、vim、JavaScript);Jekyll/jekyll、Tomcat/tomcat、JVM/jvm 等 5 组大小写重复的 tag 归一。
  • 正文标题 hover # 锚点,点击复制本节链接。

V2.29 — 2026-09-15:构建链路收口、本地检查、D1 备份

  • 样式回到单一来源:less/argan-blog.less → npm run css;手写在 css 里的几百行规则收进 theme-overrides.less / dashboard.less,less 从 2014 年的 1.7 升到 4.9(老版本会把 calc(100vh - 160px) 算成 calc(-60vh)),grunt 全部卸掉。副作用:9-14「字体优化」(font-weight: 450)这次才真正上线。
  • 文章页 12 个 <script> 合成一个 js/blog.min.js(npm run js)。
  • npm run check + pre-push 钩子,把 CI 的检查搬到本地;新增 tools/liquid-scan.py(代码里没包 raw 的 {{)和 tools/css-compare.py(两份 css 的语义比对)。
  • Worker 的 D1 每周日自动导出为 workflow artifact(第六节有恢复步骤);GA 改为直接加载 GA4 gtag,不再经 analytics.js 转发;卸掉 grunt 后 Dependabot 告警 25 → 0;两张漏网的大图(一张 BMP)转 WebP。

V2.28 — 2026-09-15:新系列《面试手撕代码》十九篇;多语言代码 tab

  • 独立系列 coding-interview(不属于三张地图):总纲 + 十三篇算法题(数组哈希、双指针滑窗、栈与单调结构、链表、二叉树、图、二分、堆区间贪心、回溯、字符串、DP 两篇、设计题)+ 六篇 AI 岗手撕(attention 家族、Transformer block 与反向传播、tokenizer 与解码、损失与训练算法、经典 ML 与指标、Infra 并发与系统)。日期排在 2025-12-01 … 12-20,落在三张地图之前、不打乱 2026 年的发布线;总纲与 AI 六篇要链到 2026 年的系列做推导延伸,带 updated: 2026-09-15。每篇:识别信号 → 模板 → 主讲题逐题推演(配图)→ 变式追问 → 两种语言的坑 → 题单 → 自测;主讲题按「高频 / 模板代表性 / follow-up 空间」三条打分选出,打分表在 labs README。
  • 算法篇每段代码 Python / Java 同处分 tab(新机制 code-tabs,写法见第三部分第 6 节);AI 篇 NumPy 实现 + --check 与 torch 对拍;Infra 篇 Python + C++。配套代码 ai-learning-labs/coding-interview/:python/(unittest)、java/(make test 用 -ea 跑断言)、ai/、infra/(Makefile)、expected/。文中每个数字(GPT-2 参数量、GEMM 计时、采样分布、AUC 等)都来自这些脚本。

V2.27 — 2026-09-15:文章页作者 GitHub 编辑入口

  • 文章页在作者通过 GitHub 登录后,在「复制为公众号格式」旁显示绿色的「在 GitHub 编辑」按钮,直接跳转 GitHub 对应的源码编辑页面。header-post 与 keynote 也使用这个操作栏;slides 不提供入口(右下角太不显眼,试过又去掉了)。不在渲染后的 HTML 与 Markdown 之间转换,也不把仓库写权限交给博客 Worker。

V2.26 — 2026-09-15:Infra 新增 10《扩散模型推理基础设施》并升入 L4 主线,平台 / 开源贡献顺延为 11 / 12

  • 地图上承诺已久的扩散推理系列写成:总纲 + 九篇(负载画像与成本账 · 单卡执行 · 跨步缓存 · 视频的长序列 attention 与稀疏化 · 多卡并行 · 少步与自回归 · serving 形态 · 三个引擎对照 · 配置评测排障),series key diffusion-inference-infra。原计划作为选修,作者判断在多模态时代它不再是选修——SGLang 与 vLLM 都已并入扩散 serving,它是推理主线的另一半——于是升入 L4 引擎层、编号 10,按”编号 = 发布顺序 = 阅读顺序”插到 09 之后:总纲 09-04、正文 09-05 … 09-13;AI 平台工程与开源贡献各后移 10 天并重编号 11 / 12(09-14 … 09-22、09-23 … 09-27),其中已上线的十篇会短期下线再重新上线(URL 与评论不受影响),这是接受的代价。开源贡献保持横切而不是选修:它是”贡献者路径”上的方法篇,地图里注明只做部署运维可跳过。
  • 不设单一源码框架:每篇末尾一张表对照 SGLang Diffusion v0.5.19 / vLLM-Omni v0.28.0 / xDiT(2026-09-02 commit 07572e7)/ diffusers v0.40.0 的实现位置,第八篇用同一个请求把三个引擎走一遍。配套只有第一篇的账本脚本 ai-learning-labs/diffusion-inference-infra/diffusion_ledger.py(纯标准库),文中数字来自它与 xDiT / SGLang 的公开实测。三张地图、全栈总览的篇数与时长(Infra 十二个系列 102 篇约 203 小时)、08 总纲末尾的指向、AGENTS 时间线同步。顺手把算法地图 L7 第六篇 HunyuanVideo 的成本粗估(把 attention 当作”与线性项相当”)改成按 \(4LN^2d\) 算的 600 PFLOPs。
  • 写法上的两个教训记入 AGENTS:行内公式必须 $$…$$(单 $ 在 kramdown 里是字面量,第一稿有 660 处);整个系列先写草稿时 front matter 要暂加 date: 才能让系列导航按顺序排。

V2.25 — 2026-09-15:处理第一批划线评论;L1 工具箱加「Python 使用层」篇

  • 读者与作者在 Discussions 里的划线评论逐条处理:正文按评论改(混合精度数据流图与 bf16 / fp16 位布局、显存账与激活的衔接、Matplotlib 用 labs 实图、三张地图「一句话」重写与「场景选型属于谁」、感受野注英文、图 1 的 Infra 边界说明、删 PyTorch 总纲的 Java 段),每条在 Discussion 下回复含「已修正」的说明(页面上标绿、周报归档);此前已回复但没写关键词的用 🎉 反应补标。待修订 Issue #27 #28 #29 关闭。
  • 采纳「L1 应像 PyTorch 上下篇一样有一篇 Python 使用层」:新写《算法工程师的工具箱(01):Python 使用层》(01-17,配 algorithm-tooling/00_python_in_use.py,只用标准库),L1 变为六篇;原 01-17 … 01-29 的十二篇各顺延一天到 01-18 … 01-30(git mv,URL 不变),Infra 01 系列现为 01-23 … 01-30。地图、总纲、AGENTS.md 里 L1 的篇号、篇数(算法专属 52 篇、约 31 小时)与引用一并更新。

V2.24 — 2026-09-17:文首问题改为脚注作答

  • 小结后那段折叠的「核心问题的答案」读起来效果不好,142 篇正文全部改为脚注:文首每个问题挂一个 [^qN],文末逐条作答(可核对的一到几句话 + 「详见第 N 章」链接),原折叠块删除;自测不变。
  • q 前缀的脚注不走浮窗(js/inline-popups.js 按 fn:q 跳过,点编号只跳转);文末脚注列表加分隔线与标签(默认「脚注」,含问答脚注时为「文首问题的答案」,2026-10-01 起改为「本文引导问题答案」),样式追加在 less/extras.less 末尾与两份 CSS。写法见第四节第 7 小节。

V2.23 — 2026-09-14:地图加时长与路线图,系列文章统一「核心问题的答案」与折叠自测

  • 以「CS 本科、数学忘光、AI 零基础、目标全栈」的读者视角审视三张地图:全栈地图「从哪一张进入」加零基础一行,「全栈的一条线」改为带每段时长的路线图;算法地图加系列依赖图与时长列;Infra 地图系列总览加时长(合计约 190 小时),十一个总纲各加「第一遍怎么读」。时长按每分钟 450 字估算。
  • 系列文章收尾结构统一为:小结 → 折叠的「核心问题的答案」(回应文首粗体问题)→ 自测(3–5 题,每题答案各自折叠)→ 下一篇。折叠用 <details markdown="1">,样式见 less/extras.less 末尾(手工追加到两份 CSS)。算法侧 57 篇与 Infra 侧 85 篇(01–11 系列的全部正文篇)都已整理;总纲不加。
  • 算法侧 L3、后训练、L6、L7 约 25 篇原本几乎没有图,按 AGENTS.md 的配图规则逐篇补 mermaid / 文本图。

V2.22 — 2026-09-14:幻灯片按文件名取日期

  • slides/ 里的 deck 是页面,Jekyll 不会像 _posts/ 那样从文件名取日期,没写 date: 的 deck 会在 ARCHIVE 顶部挂成无年份、无日期的一条。新增 _plugins/slides_date.rb:文件名有 YYYY-MM-DD- 前缀且 front matter 没有 date: 时自动补上,与文章一致;archive.html 对两者都没有的条目落到最底部的「未注明日期」。

V2.21 — 2026-09-14:算法地图 L0–L2 由三篇导读升级为三个系列,1–3 月时间线重排

  • 读者反馈三篇导读对从零开始的人不够(「根本不足以支撑读懂那八个公式」),于是展开成三个面向 AI 小白的系列,总纲沿用原导读的 URL(评论不丢):L0《算法工程师的数学》八篇(math-for-ai,01-07 总纲,01-08 … 01-15)、L1《算法工程师的工具箱》五篇(V2.25 起六篇;algorithm-tooling,01-16 总纲,01-17 … 01-21)、L2《LLM 时代的经典机器学习》六篇(classical-ml,02-27 总纲,02-28 … 03-05)。每篇从定义讲起、代真实模型算数字、篇末自测;L1 / L2 每篇配一个 CPU 可跑的脚本(ai-learning-labs/algorithm-tooling/、classical-ml/),L0 不配。
  • Python 语言本身不在 L1 讲:Infra 的 01 Python 与 03 PyTorch 改为两张地图共享,是 L1 的「深入篇」(写法同 04),两个总纲文首各加一句说明;算法地图 L1 一节、Infra 地图 01 / 03 一节、全栈总览「共同的前置」段同步。
  • 为让发布顺序 = 学习顺序,重排 3 月 23 日之前的文章(24 个 git mv,URL 不变):01-02 算法地图 ↔ 01-03 Infra 地图互换;L0 → L1 → 01 Python(01-22 … 01-29)→ 02 C++(02-02 … 02-15 不动)→ 03 PyTorch(02-16 … 02-26)→ L2 → L3(03-23 不动)。
  • L3 等系列里「L0 / L1 / L2 导读第 N 章」的引用改为指向新系列对应篇;行内公式不能有裸 |(kramdown 会把整段当表格)记入 AGENTS.md。

V2.20 — 2026-09-14:统一点赞文案,简化反馈入口与统计展示

  • 「有用」和划线工具条中的「赞」统一改为「点赞」;各级标题后的章节反馈按钮、COMMENTS 前的文章操作栏也统一使用「点赞」。
  • 移除划线工具条的「建议修改」入口及对应徽章 / 预填模式;需要提出修改建议时直接使用普通「评论」,历史评论保留。
  • 首页、Life 列表和文章详情页顶部的阅读、点赞、评论、分享统计改为文字 + 数字,不再使用图标数字角标。

V2.20 — 2026-09-14:《RL 后训练基础设施》八篇写完

  • 正文八篇(08-27 … 09-03)一次写完并挂进总纲目录:负载画像 / 系统形态 / 共置 / 权重同步 / 异步与 off-policy / Agentic rollout / verl 源码导读 / 配置与排障。源码事实取自 verl v0.9.0、vLLM v0.27.1、slime v0.3.0、AReaL 的浅克隆,只引路径与函数名、不引行号;公开数字(verl fully_async 与 delta_weight_sync 文档的实验表、checkpoint engine 基准)注明出处。
  • 只有第一篇有配套脚本(rl_ledger.py);后面各篇不再写配套代码——作者的要求:配套实验费时、文章反而写薄。各篇末尾改为一段「实践建议」。总纲的「贯穿全系列的实践线」相应改写。
  • Infra 地图现状表、全栈总览的系列数(十一个系列 93 篇)与 AGENTS.md 同步。

V2.19 — 2026-09-14:Infra 地图新增「RL 后训练基础设施」并重排发布顺序

  • 「RL 后训练基础设施」从选修升为主线 L4(与 07、08 并列),编号 09;发了总纲《RL 后训练基础设施:rollout 与训练如何共享一组 GPU》(series.yml 的 rl-post-training-infra),八篇正文陆续写,总纲的章节目录先不带链接。升主线的理由写在 Infra 地图 09 一节:编排本身是新机制,框架的机制层已经稳定。源码对象只选 verl(事实标准、一套代码覆盖全部形态),slime / AReaL 只在第七篇末节对照。
  • 为了保持「发布顺序 = 学习顺序」,把系列重排:vLLM 十四篇压成每天一篇(08-11 … 08-25;不能更早,它 pin 的 vLLM v0.27.1 tag 就是 08-11);RL 总纲定在 08-26、正文 08-27 … 09-03;原 09 平台 → 10(09-04 … 09-12)、原 10 开源贡献 → 11(09-13 … 09-17),文件改名、URL 不变。副作用:开源贡献 15–17 日的三篇在到期前暂时下线。地图、全栈总览、算法 / 应用地图与后训练系列里的编号与「选修」措辞同步。
  • 「扩散模型推理基础设施」仍列选修,计划作为五篇的短系列补上。

V2.18 — 2026-09-14:按读者反馈拆系列

  • 读者在《AI 全栈学习地图》划线问「前八篇 / 后四篇既然分明,为什么不拆成两个系列」——说得对,一个系列应当自治。《Transformer 与 LLM》收回到 8 篇(成本表);原 09–12 独立为新系列《预训练:从 tokenizer 到训练配方》(_data/series.yml 的 pretraining,新写一篇总纲),只属于算法地图 L4。文章 URL 不变,只改标题与 series:,旧评论不受影响;三张地图与全栈总览同步。
  • 同一位读者指出全栈总览里的 harness 一词没有说明出处:按「更新说明」规则加了 更新 @2026-09-14 与 updated:,并链到应用地图的「三个 harness 案例」。这是「读者划线 → Issue → 修文 → Fixes #N 变绿」链路第一次完整跑通。

V2.17 — 2026-09-13:代码块的评论方块、图上的按钮归位

  • 代码块右上角复制按钮旁新增和图片一样的评论方块:一点选中整段代码,弹出普通划线工具条(点赞 / 存疑 / 评论 / 复制 / 搜一搜 / 分享)。
  • 图片 / Mermaid 图的复制与评论按钮改挂在图本身的右上角(之前挂在整栏的右上角,图居中时按钮飘在空白处,几乎点不到),悬停出现,手机常显。

V2.16 — 2026-09-13:反馈自动进队列、章节级反馈、去 jQuery

  • 每周一 feedback-queue Action 用仪表盘同一套简报逻辑(抽成 js/feedback-brief.js)跑全站,反馈够多的文章自动开 / 更新 / 关闭一个「待修订」Issue,正文就是简报,AI 工具可直接接活。Worker GET /feedback 不带 path 时返回全站。
  • 图片和 Mermaid 图有了图题(图 N:alt / %% 图:…)和右上角「评论」方块:点方块选中图题、弹划线工具条,图题就是这张图的锚点;有评论的图加一圈边框(js/figures.js)。alt 从此必填。
  • 每个标题(各级章节)末尾加「点赞 / 没看懂」两个小按钮:匿名、不用选文字,存 passage_reactions(quote = § 标题)。简报开头新增「章节热度」表,仪表盘的句子列表给这类行打「章节」标签,「待处理」区先列「待修订」Issue。
  • 全站去掉 jQuery 2.1.3、Bootstrap 3 JS 与 FastClick;argan-blog.js、标签云改原生。Bootstrap CSS 保留。

V2.15 — 2026-09-13:把读者反馈变成可执行的修订任务

  • 「存疑」可以再点一个原因(有错误 / 没看懂 / 版本过时 / 缺例子/图 / 与前文矛盾;POST /reactions kind=reason,passage_reactions.reasons),每条 reaction / 划线评论记下所在章节(section 列 / 评论头 · 位于「…」)。
  • 后续已移除「建议修改」快捷入口;读者改用普通「评论」提交问题和建议。
  • 「已修正」状态:带 Issue 的看 Issue 是否关闭;不带的看作者回复「已修正」或 🎉。命中后划线变绿实线、标记带 ✓、评论挂徽章。定位不上的划线评论改为显示引文片段。
  • 仪表盘新增「修订简报」(#brief=/slug.html,文章榜每行有入口,Worker GET /feedback):一篇文章的全部反馈汇成 Markdown,附给 AI 的修订指令,一键复制。「读者划出来的句子」显示章节和存疑原因。
  • 评论编辑器的 Markdown 工具栏加「表格」按钮(插入 3×3)。

V2.14 — 2026-09-12:分享一句话,修引文转义

  • 工具条末尾的「复制链接」改成「分享」按钮:弹出与整篇文章相同的分享菜单(系统分享 / 微博 / X / LinkedIn / 微信二维码 / 复制链接,js/share.js 暴露的 window.BlogShare),链接指向这句话(已有讨论则指向讨论串);讨论面板的「赞」「存疑」旁也有「分享 N」。每次分享给这句话记一次(passage_reactions.share,POST /reactions kind=share),同时也算整篇文章的一次分享。仪表盘「读者划出来的句子」新增「分享最多」。
  • 修 bug:以数字加点开头的引文(如 1.5px …)发到 GitHub 时转义写成了 \1.5px,多出一个反斜杠,hash 对不上,「§ 原文位置」跳不回去。现在转义为 1\.5px(之前发出去的那条要手动改掉引文里的反斜杠)。
  • 仪表盘文章榜默认只列 TOP 10(长尾全是阅读 1 的文章,没有信息量),底部可展开全部;合计仍为全部文章。

V2.13 — 2026-09-11:给一句话点赞 / 存疑,仪表盘加视角

  • 选中文字的工具条新增「赞」「存疑」:匿名、不用登录、一个浏览器一票(Worker GET/POST /reactions,D1 passage_reactions)。没有评论的段落也能被划线;存疑为红色点线;段尾标记合成 💬 / 👍 / ❓;「最受关注的段落」公式加入赞与存疑。
  • 仪表盘:阅读趋势(新表 views_daily,按北京日期)、文章榜(阅读 / 有用 / 有用率 / 分享 / 评论,可排序)、读者存疑 / 点赞最多的句子;修了「最近评论」一直登不上的 bug(GraphQL 别名和字段同名冲突)。

V2.12 — 2026-09-11:角标计数与分享数

  • 头部 meta 行和首页 / /life/ 列表的统计改为文字 + 数字:阅读、点赞、评论、分享;列表页去掉全部动作按钮。
  • 新增分享次数(Worker POST /shares,D1),分享菜单任一项用过即加一。
  • 划线评论的高亮从黄色底改为微信读书式的虚线下划线(琥珀色,区别于作者提示的青色虚线;同一段多条讨论为实线,悬停才有淡淡底色)。
  • 版权声明改为 CC BY 4.0:保留作者、署名和原文链接即可转载、翻译或商业引用(_config.yml license.note,%url% 代表本文链接)。

V2.11 — 2026-09-10:版权声明与侧栏

  • 每篇文章末尾加版权声明(_config.yml license:,默认 CC BY-NC-SA 4.0;单篇可 license: false)。
  • 侧栏文字加深、ABOUT ME / HOT TAGS / RECOMMEND 改成带下划线的小标题;页脚去掉文章链接,加 GitHub Star(带数量);归档页 Go to article 的放大镜换成 Font Awesome 图标。

V2.10 — 2026-09-10:操作栏与分享

  • 操作栏「有用 · 分享 · 复制为公众号格式」放到 COMMENTS 上方;「有用」改为匿名计数(Worker /votes,D1),不再是需要 GitHub 登录的讨论串 👍。头部 meta 行加「N 人觉得有用 · N 条评论」,首页 / /life/ 列表每篇带「N 人觉得有用」(Worker 新增 GET /stats)。
  • 「分享」菜单:系统分享 / 微博 / X / LinkedIn / 微信二维码 / 复制链接,零第三方脚本。
  • 作者专用「复制为公众号格式」:正文转内联样式 HTML 进剪贴板,外链变脚注、公式转图、webp / Mermaid 转位图,粘进公众号或知乎编辑器即可。

V2.9 — 2026-09-10:Tech / Life 分栏

  • 导航 Posts 改为 Tech,与 Life 并列:首页只放技术文章,生活文只在 /life/(_plugins/home_flow.rb 给 life / 置顶文章打 hidden,jekyll-paginate 据此跳过,分页不再有空洞)。去掉 meta 分类,备忘录 / 读者信 / 布局演示当普通文章进首页流。归档标记 [Tech] / [Life]。
  • 新文《个人博客终于迎来了久违的更新》(生活类,带全套功能截图)。

V2.8 — 2026-09-09:两篇说明书

  • 选中文字的浮动工具条新增「复制」「搜一搜」(Google)。
  • 分类:category: life,首页徽章、/life/ 卡片页、导航 Life、归档标记、页脚说明书入口;作者仪表盘 /admin/stats.html(Worker 新增 GET /views/top)。
  • 首页置顶(pinned: true);文章头部 meta 加「N 人觉得有用」;侧栏 RECOMMEND 改为带推荐理由的外链列表;HOT TAGS 阈值改用 featured-condition-size;上下篇与相关文章之间加间距。
  • 《博客备忘录》(本文,并入原浮窗脚注演示文)与《致读者的一封信》,其余演示文删除、旧地址 301(jekyll-redirect-from)。
  • header-post 布局新增 header-bg-css / header-img-credit;tools/new-post.py 建文脚本。
  • 删除停服的 JiaThis 分享按钮;keynote 布局补齐目录 / 相关文章 / 评论区并清掉多说残留,新增《keynote 布局演示》。

V2.7 — 2026-09-09:体积与图标

  • img/in-post/ 全部转 WebP(20 MB → 5.8 MB),tools/webp-images.py。
  • Font Awesome 改为自托管子集(77 KB → 20 KB,固定含约 150 个常用图标,CI 兜底)。
  • 评论区「最受关注的段落」;作者徽章;回复通知提示。
  • 修复:lychee 之前因一条注释里的 <pre> 只检查了 <head>——修好后全站 2 万个链接真正过检,顺手修了 7 个老文章的坏链接。

V2.6 — 2026-09-09:发布链路与 SEO

  • 部署改为 GitHub Actions(Jekyll 4.4,与本地一致);每日定时构建实现定时发布;timezone: Asia/Shanghai。
  • CI:check(构建 + 站内链接 / 锚点 + headless Chrome 渲染检查)、external links(每周,开 Issue)。
  • Open Graph / Twitter Card / JSON-LD、RSS 自动发现、强制 HTTPS、url 改 https;去掉无效的 no-cache meta 与停服 CDN;静态资源按构建版本缓存(搜索索引不再每次重下 10 MB)。
  • 文章头部 meta 行:updated、阅读时长 / 字数、阅读数;正文图片 lazy 加载并可点击放大。
  • 系列文章改为 series: front matter + _data/series.yml 自动导航,84 篇迁移。

V2.5 — 2026-09-09:点赞、投票、阅读数

  • 文章「有用」= Worker + D1 匿名计数;评论 ▲ 分数 ▼ = GitHub 👍 / 👎 reaction,一人一票;阅读数 = Worker + Cloudflare D1。
  • 用户可见文案统一为「评论 / 划线评论」。

V2.4 — 2026-09-08 ~ 09:划线评论与自建评论区

  • 读者选中正文即可就地评论(W3C TextQuoteSelector + 模糊匹配定位,原文小改不丢);段落下方就地展开讨论串,同段落合并。
  • 文末评论区不再用 giscus iframe,改由同一段脚本渲染:回复、编辑、删除、Markdown 工具条与预览、GitHub 登录、草稿保留。
  • 「同时提交 Issue」:Worker 以博客自己的 GitHub App 身份建 Issue(取代会过期的 PAT),评论带红旗徽章。

V2.3 — 2026-09-08:作者侧解释与评论迁移

  • 浮窗脚注、行内 Tips(四种写法)、站外链接自动识别。
  • 评论从 Disqus 迁到 giscus(GitHub Discussions)。

V2.2 — 2026-08-27:搜索

  • 站内全文搜索(前端索引,打开时才加载),修复卡顿;首页与右侧栏显示优化。

V2.1 — 2026-08-19:幻灯片

  • slides 布局:Markdown 写 reveal.js 演示文稿,/slides/ 索引。

V2.0 — 2026-08-16 ~ 17:阅读体验

  • Mermaid 图(浏览器渲染、点击放大)、KaTeX 公式;[TOC] 与右侧浮动目录;文章页排版与代码块风格对齐 GitHub;代码块复制按钮;导航栏修复与放大。

V1.x — 2017 ~ 2026-08

  • 2017-05 从 Octopress 迁到 Jekyll 与现在的主题;Disqus 评论;随笔用大图头部(header-post);PWA / Service Worker(后来关闭);侧栏、HOT TAGS、SEOTitle、GA / 百度统计。

  1. NCCL (NVIDIA Collective Communications Library):英伟达专为 GPU 集群优化的集合通信库,实现了跨 PCIe、NVLink 和 InfiniBand 的广播、归约与 AllGather。详见 NCCL 官方仓库。 ↩

  2. Ring AllReduce:每个进程只与左右邻居通信,把数据切成 \(S/N\) 大小的块环状传递,总通信量与节点数 \(N\) 无关。 ↩

  3. vLLM 是伯克利推出的高效 LLM 推理与服务引擎。

    核心创新是借鉴操作系统虚拟内存分页思想的 PagedAttention,把显存浪费从 60%–80% 压到 4% 以下。 ↩

  4. PyTorch C++ 算子实现骨架——脚注里可以放代码块:

    #include <torch/extension.h>
    torch::Tensor custom_add(torch::Tensor a, torch::Tensor b) {
        return a + b;
    }
    PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
        m.def("forward", &custom_add, "Custom Add Forward");
    }
    

    ↩

  5. 三种并行策略速查——脚注里也可以放表格:

    并行策略 切分对象 主要通信算子
    张量并行 (TP) 权重矩阵 ($W$) All-Reduce
    流水线并行 (PP) 网络层数 P2P (Send/Recv)
    数据并行 (DP/ZeRO) 批量样本 Reduce-Scatter / All-Gather

    ↩

这篇对你有用?

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/blog-memo.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×