Files
blog/docs/写作指南.md
wait4xandGLM-5.3 aaa9cd9274 feat(typography): 全站三层排版——富文本/页面级覆盖/艺术字体类
- 层 1 所有页面文案改富文本:site-content 新增 renderRich(md,{inline})
  复用既有全管线(raw→内容净化→class 白名单→链接卡片);接入
  home.tagline/subtitle、opensource.blurb、about.intro、blocks prose;
  新增 .rich 容器作用域并与 .prose-body 合并(77 处选择器覆盖四类容器)
  坑:renderRich 输出 <p> 塞进 <p class="tagline"> 会被浏览器拆散 →
  行内场景用 {inline:true} 剥外层 <p>
- 层 2 页面级排版覆盖:三单例加 typography{font?,accent?,scale?},读取器
  pageTypography() 走白名单(10 字族 / 18 色类,scale 夹 0.8–1.25,清单外
  忽略);页面容器挂 .page-typo + CSS 变量 --page-font/--page-accent/
  --page-scale,不动全站 tokens;后台三页加「页面排版覆盖」下拉
- 层 3 艺术字体:新增 font-art-kuaile(快乐体专用类,字距微调),白名单与
  可视化面板同步;站酷高端黑/阿里妈妈东方大楷/演示悠然小楷因无 npm 包或
  嵌入条款风险进候补清单
- 默认值清理:about.yaml 的 typography 测试值(整页汇文)清为注释示例——
  整页换字族会带来 694KB 字体成本,默认留空=全站默认排版

验证: 构建 48 页 exit 0;E2E 37/37;三页 page-typo 挂载与变量实测、四页
      (图库/书架/文章/装备)隔离为 0、font-art-kuaile 生效、about 清空后
      无覆盖

Spec: none (排版三层,先于 5g 的任务;T3/T4 另行交付)

Co-authored-by: GLM-5.3
2026-11-02 18:40:32 +08:00

332 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 写作指南:在文章里插入图片 / 视频 / 音频
> 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。
## 目录约定
```
content/posts/
├── my-post.md # 文章
└── assets/
└── my-post/ # 与文章 slug 同名:该篇私有资产
├── photo.jpg
├── clip.mp4
└── voice.mp3
```
相对引用一律 `./assets/{slug}/文件名`,构建时 Astro 图片服务自动出多尺寸 webp。
## 图片
标准 markdown 语法,alt 必填(无障碍 + 暗色降亮依据):
```markdown
![外滩夜景](./assets/my-post/bund.jpg)
<p class="fig-cap">图:外滩的傍晚</p> ← 显式图注类(居中小字)
```
- 图注可选;**必须用 `<p class="fig-cap">…</p>`**(2026-11-02 起)
- ⚠️ 旧写法 `*斜体独立段*` 已不再自动当图注:此前它会把正文里所有独占成段的斜体都变成块级居中(用户实测的排版 bug),现在斜体恢复为正常行内强调
- 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩)
## 视频 / 音频
markdown 原生不认视音频,写 HTML 标签;**raw HTML 的相对路径不走 Astro
资产管线**(只有 `![]()` 图片语法走),所以媒体文件放 **`public/media/{slug}/`**
(构建原样拷入 dist),引用用绝对路径 `/media/...`:
```
public/media/my-post/clip.mp4 ← 文件放这里
```
```html
<video controls preload="metadata" src="/media/my-post/clip.mp4"></video>
*片头三秒试拍*
<audio controls preload="metadata" src="/media/my-post/voice.mp3"></audio>
*朗读:面纱选段*
```
- `preload="metadata"` 必写(不预拖流量,只取时长/首帧)
- **图注警示**:斜体图注行内不要裸写 `<video>`/`<audio>` 字样——markdown 会把它渲染成真实元素(黑盒+吞文字),提及时用反引号 \`<video>\`
- 样式已全局处理(视频全宽圆角黑底、音频胶囊条)
- /media/ 命名空间同时覆盖"仓内小文件"与"仓外 store 大文件"(见体积纪律表)
## 体积纪律(git 仓红线)
| 媒体 | 入仓上限 | 超限怎么办 |
|---|---|---|
| 图片 | 单张 ≤1MB(2400px 压缩后通常 200-800KB) | 重压 |
| 音频 | ≤5MB(3 分钟 128kbps ≈ 2.8MB) | 降码率 |
| 视频 | ≤5MB | 放仓外 `~/docker_apps/blog/media/`(photos-originals 同模式),文章里用绝对路径 `/media/xxx.mp4`;build.sh 同步段待该场景首次出现时补 |
> 历史教训:21 张原图 196M 直接入仓曾撞死推送通道(git 历史 7a2880b 重写才甩掉)。
## 三媒齐用的样例
见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。
## 链接卡片(OG 预览卡)
**一行只放一个链接、且它独立成段**时,构建期会抓取该页的 OG 信息渲染成卡片(标题 / 描述 / 站点图标 / 可选缩略图):
```markdown
https://astro.build/
[也可以是 markdown 链接形态](https://astro.build/)
```
规则与边界:
- **内联链接不变**:夹在句子里的链接保持普通样式(`这是一句带 [链接](https://example.com) 的话` → 不会变卡),所以不会把整篇变成卡片墙
- **抓取失败一律回落普通链接**:404、无 OG、超时(4s)、站点拒爬都不影响构建
- **每篇最多 10 张卡**,超出的回落普通链接(防止文章变卡片流)
- **缓存**:结果存 `content/.link-cache.json`(入仓,保证构建确定性);条目 30 天后自动重抓。想强制刷新某条——删掉该 URL 的条目即可
- 缩略图取 `og:image`,无图不占位;图标取站点而非第三方 favicon 服务
## 文章排版类(受限白名单)
正文里可用 `<span class="…">` / `<p class="…">` 做**受限**排版。构建期有白名单过滤:**只放行下表类名,其它 class 与 `style`、`on*` 属性一律被剥离**(防注入 + 保护灰阶骨架)。
### 灰阶层级类(全部实测亮暗对比度 ≥4.5:1)
| 类名 | 作用 |
|---|---|
| `ink-1` / `ink-2` / `ink-3` | 指定灰阶层(标题/正文/次要) |
| `ink-muted` | 最淡灰(注释感) |
| `ink-faint` | 最淡**可读**档(再淡即不达 AA) |
| `ink-warm` | 暖调正文(书评气质) |
| `ink-cool` | 冷调正文(技术气质) |
| `ink-accent` | 强调(链接色,少量点缀) |
| `ink-accent-soft` | 弱强调(比强调淡一档) |
### 语义色板(8 色相 × 标准/soft;亮暗各自取值,实测全部 ≥4.5:1)
| 色相 | 标准类 | soft 类 | 亮态对比度(标准 / soft) | 暗态(标准 / soft) |
|---|---|---|---|---|
| 蓝 | `c-blue` | `c-blue-soft` | 6.46 / 4.58 | 7.70 / 4.61 |
| 青 | `c-teal` | `c-teal-soft` | 5.00 / 4.57 | 9.77 / 4.52 |
| 绿 | `c-green` | `c-green-soft` | 5.38 / 4.67 | 10.38 / 4.67 |
| 黄 | `c-yellow` | `c-yellow-soft` | 6.45 / 4.57 | 11.73 / 4.52 |
| 橙 | `c-orange` | `c-orange-soft` | 6.26 / 4.65 | 9.44 / 4.58 |
| 红 | `c-red` | `c-red-soft` | 6.68 / 4.55 | 7.51 / 4.50 |
| 粉 | `c-pink` | `c-pink-soft` | 6.96 / 4.73 | 8.87 / 4.52 |
| 紫 | `c-purple` | `c-purple-soft` | 7.46 / 4.53 | 8.25 / 4.64 |
- 黄色在亮态被压暗到 `#7a5900`(原色在白底只有 ~1.8:1),这是**必须**的:彩色文字要能读
- `soft` 档是"阈值内最淡值",适合次要强调;标准档用于关键强调
> **使用纪律(重要)**:彩色**只作用于文章正文的强调文字**。站点外壳(导航、页脚、按钮、背景、卡片边框)一律保持灰阶——整站不因正文用色而花掉。写作时一个页面里的彩色强调建议不超过 3 处。
### 字体类
| 类名 | 作用 | 字体 | 许可证 | 加载 |
|---|---|---|---|---|
| `font-serif` | 书评正文族 | Noto Serif SC | OFL-1.1 | 自托管 |
| `font-kai` | 金句族 | 霞鹜文楷 Screen | OFL-1.1 | 自托管 |
| `font-sans` | UI/技术族 | Noto Sans SC | OFL-1.1 | 自托管 |
| `font-mono` | 标签/数值 | JetBrains Mono | OFL-1.1 | 自托管 |
| `font-song` | 宋体族 | Noto Serif SC 优先,系统宋体兜底 | OFL-1.1 | **自托管优先** |
| `font-kai-sys` | 楷体族 | 霞鹜文楷优先,系统楷体兜底 | OFL-1.1 | **自托管优先** |
| `font-hei-sys` | 黑体族 | Noto Sans SC 优先,系统黑体兜底 | OFL-1.1 | **自托管优先** |
| `font-fangsong` | 仿宋族 | FangSong/STFangsong | — | ⚠️ **纯系统回退**(暂无 OFL 自托管仿宋包) |
### 文学排版类(段落级)
| 类名 | 作用 |
|---|---|
| `drop-cap` | 段落首字下沉(书评开篇) |
| `quote-line` | 金句块(左侧竖线 + 文楷) |
| `fig-cap` | 图注(居中小字) |
### 首批字体(已落地,6 族 + 3 档自托管优先)
| 类名 | 字体 | 许可证 | webfont 嵌入 | 用到该字体的页面增量 |
|---|---|---|---|---|
| `font-serif` | Noto Serif SC | OFL-1.1 | ✅ OFL 允许 | 基线内 |
| `font-sans` | Noto Sans SC | OFL-1.1 | ✅ | 基线内 |
| `font-kai` | 霞鹜文楷 Screen | OFL-1.1 | ✅ | 基线内 |
| `font-mono` | JetBrains Mono | OFL-1.1 | ✅ | 基线内 |
| `font-huiwen` | 汇文明朝体(文风切片包 339 片) | OFL-1.1 | ✅ | **535KB** |
| `font-kuaile` | 站酷快乐体 | OFL-1.1 | ✅ | **163KB** |
| `font-song` / `font-kai-sys` / `font-hei-sys` | 上述自托管字体优先,系统字族兜底 | OFL-1.1 | ✅ | **+0KB** |
体积账结论(2026-11-02 实测,本地无 gzip 的相对值):**未使用某字体的页面,字体字节增量恒为 0**(构建期按页子集化 + unicode-range 按需拉取)。汇文采用文风开源的 cn-font-split 切片包(339 片),实测只拉用到字符的 19 片。
### 候补字体清单(未落地,加一款约"一条命令 + 一次体积账")
| 字体 | 许可证 | 允许 webfont 嵌入 | 未落地原因 |
|---|---|---|---|
| HarmonyOS Sans | 华为字体许可(免费商用) | ✅ 明文允许"嵌入、捆绑、重新分发",需标注 | npm 无包,官网整包 52MB |
| MiSans | 小米字体许可(免费商用) | ✅ 官方 FAQ 明确 "embedded fonts: Yes",需注明 | npm 无包 |
| OPPO Sans | 免费授权(含商用) | ✅(附带"不向他方提供下载渠道"限制) | npm 无包 |
| 阿里巴巴普惠体 | 免费商用全场景 | ✅ | npm 无包,9 字重体积巨大 |
| 得意黑 Smiley Sans | OFL-1.1 | ✅ | 仅 GitHub release 整包,直链在当前网络不可达 |
| 更纱黑体 Sarasa Gothic / Sarasa Mono | OFL-1.1 | ✅ | npm 包为空壳(需 cli 自生成切片) |
| 花园明朝 HanaMinA/B | IPA Font License | ✅(允许嵌入) | 无 npm 包,仅兜底用 |
| 站酷高端黑 / 站酷快乐体(已落) | OFL-1.1 | ✅ | 高端黑无 @fontsource 版本 |
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
| 霞鹜文楷 Light/Bright | OFL-1.1 | ✅ | 实测 **+8605KB**(CJK 切片粒度所致) |
> 加新字体的流程:①核许可证与 **web 嵌入条款**(很多"个人免费"字体明文禁 @font-face,只能进系统回退)②npm 装或放入 `public/fonts/<family>/`(优选 cn-font-split 类切片包)③`global.css` 加类 + `rehype-class-whitelist` 放行 + `public/admin/typography-classes.js` 加一项 ④跑体积账,超 150KB/页 的要评估是否值得。
```markdown
<span class="ink-muted">次要说明</span>
<span class="font-kai">金句可以换文楷</span>
<p class="drop-cap">开篇段落,首字会下沉。</p>
<p class="quote-line">所谓暮色,就是光慢慢地承认自己要走。</p>
```
- **颜色只能走上面这些类**(内部用 tokens,亮暗双态都可读);写 `style="color:red"` 会被构建期剔除
- 斜体就用普通 markdown `*斜体*`——行内强调,不会换行也不会居中
### 后台可视化选择(推荐,不用写 HTML)
后台编辑器里:**选中文字 → Insert 菜单 → 「排版(字体/颜色)」→ 面板里点一个类**,文字即被 `<span class="…">` 包住;点已套用的片段可改类,Backspace 可整体删除。
面板里的两类与上表一一对应(字体 8 / 颜色 9),类清单单一事实源在 `public/admin/typography-classes.js`。
## 三层排版架构(全站,不只文章正文)
**层 1 · 页面文案富文本**:后台「页面文案」里的 tagline / subtitle / blurb / blurb_after / intro / 关于页 blocks 的 prose 块,全部走同一条富文本管线(markdown + raw + 内容净化 + class 白名单 + 链接卡片)。可以直接写:
```yaml
tagline:
zh: '<span class="font-kai c-blue">写作者</span> · 读者 · 快门按压者,拥抱开源。'
```
渲染入口唯一:`src/lib/site-content.ts` 的 `renderRich()`(行内场景传 `{ inline: true }` 自动剥外层 `<p>`,否则 `<p>` 套 `<p>` 会被浏览器拆散)。
**不开放**:导航/页脚/按钮等硬编码界面外壳——那是 UI 不是内容。
**层 2 · 页面级排版覆盖**:每个页面单例(`content/site/*.yaml`)可加:
```yaml
typography:
font: font-kai # 本页正文默认字体类(可选)
accent: c-blue # 本页强调色类(可选)
scale: 1.1 # 字号缩放 0.8–1.25(可选)
```
实现是在该页内容容器上挂 `.page-typo` + CSS 变量(`--page-font / --page-accent / --page-scale`),**不写全站 tokens**——其它页面零影响(已在图库/书架/文章/装备页验证隔离)。白名单外的类名会被忽略。
后台入口:「页面文案 → 各自页面 → 页面排版覆盖」。
**层 3 · 艺术字体**:`font-art-kuaile`(站酷快乐体,艺术字专用类)。⚠️ **仅用于标题/短句**:艺术字字形大,CJK 切片体积高(实测用到页 +163KB),长段使用会显著抬高单页字体成本。
## 发布提醒
- push main → webhook 自动构建发布(约 10 秒)
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
## 书封图(/books 书架)
阅读轨文章可选加书封:
1. 图片放 `content/posts/assets/books/<slug>.jpg`(建议宽 480px、高宽比约 2:3)
2. 对应文章 frontmatter 加一行:`cover: ./assets/books/<slug>.jpg`
3. 构建自动优化出 96/192 两档;**没有 cover 的文章优雅回退现有色脊卡设计**,无需改任何代码
示例 frontmatter:
```yaml
---
title: "面纱"
pubDate: 2024-06-01
category: reading
bookAuthor: "毛姆"
mood: "#8a6f4a"
cover: ./assets/books/mian-sha.jpg
---
```
## 后台上传(Sveltia 后台,推荐路径)
后台地址 `/admin/`(gitea 账号登录)。三类媒体都有后台入口,不必手写路径:
### 图片
文章编辑器里插入图片 → 自动上传到 `content/posts/assets/` 并写入 `./assets/...`
相对路径(走 Astro 图片管线,自动优化)。**不要手改成 `/assets/...` 绝对路径**。
### 视频(frontmatter 字段,正文后自动渲染)
1. 文章编辑页「附视频」字段上传(自动落 `public/media/`,路径写 `/media/<name>.mp4`)
2. 正文读完后自动出现 `<video controls preload="metadata" playsinline>`,全宽圆角黑底
3. 「视频封面」填 `/media/xxx.jpg` 可自定首帧;留空浏览器自取
4. 无视频字段 → 该区块不渲染(零占位)
体积纪律:mp4/webm ≤50MB 走 `public/media/`(构建原样拷贝,不转码);
更大的请按上文「体积纪律」放仓外 store(该场景 build.sh 同步段待首次出现时补)。
### 后台列表日期
文章/瞬间列表显示「日期 · 标题」,可按日期排序;图库照片没有 frontmatter 日期
(时间在 EXIF/文件侧),列表按「分组 · 标题」展示、按标题/分组排序。
### 链接
markdown 原生语法,无需任何配置:
```markdown
[文字](https://example.com)
```
想让链接显示成 OG 预览卡(带标题/描述/图标),把它**单独放一行**即可——规则见上文「链接卡片(OG 预览卡)」。
### 页面文案(主页/关于/开源)
后台「页面文案」下三个入口:
| 入口 | 文件 | 可改内容 |
|---|---|---|
| 主页设置 | `content/site/home.yaml` | 身份语 tagline、**头像 avatar**、**站点图标 favicon**、**社交链接 socials** |
| 关于页 | `content/site/about.yaml` | **markdown 正文 intro**(多段/列表/粗斜体/链接) |
| 开源页 | `content/site/opensource.yaml` | 叙述 blurb/blurb_after、仓链接、置顶项目 pinned |
#### 头像与站点图标
- **头像**:主页设置里「头像」上传 → 落 `content/site/assets/`,构建期优化成 60px WebP;
留空则显示内置渐变占位(w4 徽标)
- **站点图标 favicon**:先经「图标」媒体库上传(`public/icons/`),再把路径粘到
「站点图标」字段(如 `/icons/my.ico`、`/icons/my.svg`);留空回退 `/favicon.svg`。
支持 `.svg` / `.ico` / `.png`(直出,不经图片优化)
#### 社交链接
主页设置里「社交链接」是列表:可增删、拖动排序。每条 = 名称(无障碍文字)+ 链接 + 图标。
图标可选 `github / rss / email / bilibili / x`,**留空则显示纯文字按钮**。
首页名片区、以及任何引用该组件的页面都吃同一份数据(单一事实源,改一次全站生效)。
#### 首页可配项(home.yaml)
| 字段 | 取值 | 默认 | 作用 |
|---|---|---|---|
| `subtitle` | `{zh, en}`(markdown 短文本) | 空 | 身份语下方的副标题;留空不渲染 |
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
#### 开源页项目墙(opensource.yaml)
| 字段 | 取值 | 默认 | 作用 |
|---|---|---|---|
| `mode` | `all` / `only` / `exclude` | all | 可见性模式 |
| `include` | 仓短名数组 | 空 | mode=only 时的白名单 |
| `exclude` | 仓短名数组 | 空 | mode=exclude 时的黑名单 |
| `sort` | `pinned-first` / `stars` / `updated` | pinned-first | 排序方式 |
| `perSource` | `both` / `github` / `gitea` | both | 来源过滤 |
| `intro_from` | `"off"` / `github-readme` | off | 开篇文案来源 |
> `intro_from` 建议**带引号写**:YAML 1.1 里裸 `off` 会被解析成布尔 false。
> 选 `github-readme` 时构建期取 `wait4xx/wait4xx` 的 README 渲染为开篇,
> 成功后缓存到 `content/.github-readme.md`(入仓);token 缺位/取失败/仓无 README
> 都会打印日志并回落 yaml 的 blurb。注意 README 里的相对链接与外部徽章会原样输出。
#### 首页文章列表
首页现在分两段:**阅读轨 6 条 + 技术轨 6 条**(各自带「全部文章」出口:阅读→书架、技术→归档)。
照片轨文章不进首页(在图库看)。完整列表见 `/archives/`。
- 中文/英文各一个字段,页面按语言取;**留空则该字段回落默认文案**(构建不会炸)
- `pinned` 填仓库短名(如 `blog`、`pi-board`),**顺序即展示顺序**;留空回落 `blog`