Merge branch 'spec/2026-10-25_add-media-and-polish'
媒体支持(图/视频/音频+写作指南)+ 灯箱胶囊按钮 + 评论区美化(Artalk.css 缺陷修复与 tokens 主题);六任务机器验证+视觉双轮收敛。 Spec: 2026-10-25_add-media-and-polish
This commit is contained in:
commit
f1ee07838d
19 files changed
+492
-35
No files matched your search
@@ -0,0 +1,93 @@
|
|||||||
|
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-10-25 12:26
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 视频进 git 仓在短小(<5MB)场景可接受;大视频走仓外 store(photos-originals 同模式)+ /media/ 路径
|
||||||
|
- Artalk 页内渲染可被页面级 CSS 覆盖定制(同源同文档)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 灯箱原图/下载按钮美化
|
||||||
|
- T2 Artalk 评论区美化
|
||||||
|
- T3 markdown 媒体支持
|
||||||
|
- T4 三媒样张
|
||||||
|
- T5 写作指南
|
||||||
|
- T6 验证收官
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简单可行方案:原生标签 + global.css 媒体样式段 + 样例验证 + 指南文档
|
||||||
|
- 暂不引入:rehype 插件链、指令语法、媒体转码管线
|
||||||
|
- 不做的抽象/配置化:多媒体源管理界面
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 图库页媒体扩展、字幕、播放列表
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 图库页媒体扩展、字幕、播放列表
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:6/6
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-10-25 12:26
|
||||||
|
- 范围:媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化
|
||||||
|
- 功能:T1 灯箱原图/下载按钮美化;T2 Artalk 评论区美化;T3 markdown 媒体支持;T4 三媒样张;T5 写作指南;T6 验证收官
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:b9dbece2dfc7e3726154a07ac11599329f73adff..76b6b6b0228c5f30a0f5c24f57c1e24f9a5f50c7
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
媒体工作流纪律:图片走 markdown ![]() 管线(自动 sharp 优化),视音频走 public/media/{slug}/ + 绝对 /media/(raw HTML 相对路径不经打包器——实测 404);斜体图注行内禁裸写 <video>/<audio> 字样(markdown 渲染成真元素:黑盒+吞文字),提及用反引号。hidden 标签=可访问不列出(列表/RSS 过滤、详情页全量出页、prev/next 滤)。Artalk 样式必须手动 link dist/Artalk.css(JS 不自动注入);18 个 --at-color-* 映射 tokens 自动双态。python replace 改源码必须 assert(静默失配=死代码上线,本次灯箱胶囊两轮翻车根因)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"lb-orig\" dist/gallery/index.html", "exit": 0, "duration_s": 5.364, "stdout_tail_sha256": "6eae0335cafe0d1e24f54d2da2faad789aafb149520c6e7aa6dd887e5408e6c9", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:26+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "pnpm astro build && grep -q \"atk-\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 4.752, "stdout_tail_sha256": "bfa0df7049956ce7f88c15baf9d92aa6a666cea3e6c52e29b71aef53fab2f992", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:30+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "pnpm astro build", "exit": 0, "duration_s": 4.768, "stdout_tail_sha256": "8c2286b96c36ba938681b73128e68200ebd4a30b31a682590d0eeee50413fae8", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:35+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "test -s docs/写作指南.md", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:35+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"lb-orig\" dist/gallery/index.html", "exit": 0, "duration_s": 5.77, "stdout_tail_sha256": "ba3cb065938be0602c31bcbc1567b8ca2ccd336c7f33dc1edf61524e10e21014", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:25:52+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "pnpm astro build && grep -q \"atk-\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 5.493, "stdout_tail_sha256": "e7c1ec86a87016deb1872ac1a525ea846811d3e4b2f5e204d578703383e30e97", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:25:58+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "pnpm astro build", "exit": 0, "duration_s": 5.307, "stdout_tail_sha256": "fcb5b66f3f505b9f2dee356714459b8146a5556bc711ecc5b52f17eb6f171629", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:03+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "pnpm astro build && grep -q \"<video\" dist/archives/*/index.html -r | head -1", "exit": 0, "duration_s": 5.383, "stdout_tail_sha256": "defdcb3c46daab3549b8e7b9d5b3815f0e4fb841522d7112ee587b1f1e4b188a", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:09+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "test -s docs/写作指南.md", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:09+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 15.076, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:24+00:00"}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:书评/技术文 markdown 支持插入图片、视频、音频(含样式与工作流文档);灯箱原图/下载按钮与 Artalk 评论区视觉美化对齐站点设计系统
|
||||||
|
- **目标用户**:站长(多媒体写作能力)与读者(一致的视觉体验)
|
||||||
|
- **核心价值**:内容表达力补全(三轨中的摄影/技术教程常需媒体)+ 收尾两处观感
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- ffmpeg 7.1.5 可用(测试媒体生成)
|
||||||
|
- Astro 5+ 内容集合 markdown 正文相对路径图片走图片服务优化;raw HTML(video/audio 标签)默认透传
|
||||||
|
- Artalk 2.x 支持 CSS 变量主题(--at-* 系)与 darkMode 配置
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 视频进 git 仓在短小(<5MB)场景可接受;大视频走仓外 store(photos-originals 同模式)+ /media/ 路径
|
||||||
|
- Artalk 页内渲染可被页面级 CSS 覆盖定制(同源同文档)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 图/视/音三媒都要 → 用户明示
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 视频/音频用原生 <video>/<audio> 标签(markdown raw HTML)> 自研指令解析器(:::video 语法)-> 选原生(零解析器、写作直观、降级天然)
|
||||||
|
- 评论区美化走页面级 CSS 覆盖 > fork Artalk 主题 -> 选覆盖(升级安全)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 灯箱原图/下载按钮美化
|
||||||
|
- [x] T2 Artalk 评论区美化(tokens 对齐 + 明暗双态)
|
||||||
|
- [x] T3 markdown 媒体支持(图片优化+样式+图注;video/audio 原生标签+统一样式)
|
||||||
|
- [x] T4 测试样张(ffmpeg 生成音视频样例 + 含三媒的样例文章)
|
||||||
|
- [x] T5 写作指南(docs/写作指南.md:三种媒体的插入方法/体积纪律/目录约定)
|
||||||
|
- [x] T6 E2E + 视觉复核 + 收官上线
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 文内图片点击放大(复用灯箱)——二期
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 图库页媒体扩展、字幕、播放列表
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简单可行方案:原生标签 + global.css 媒体样式段 + 样例验证 + 指南文档
|
||||||
|
- 暂不引入:rehype 插件链、指令语法、媒体转码管线
|
||||||
|
- 不做的抽象/配置化:多媒体源管理界面
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro markdown 原生能力 + CSS
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/styles/、src/components/(ArtalkComments/GalleryPage)、content/posts(样例)、docs/、tests/
|
||||||
|
- 本轮不应触碰:infra 生产面、content/posts 既有六篇正文
|
||||||
|
- Git integration branch:`spec/2026-10-25_add-media-and-polish`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:媒体样式与组件、样例内容、指南文档
|
||||||
|
- 快速检查:`pnpm astro build`
|
||||||
|
- 集成检查:`node tests/e2e.mjs`
|
||||||
|
- 全项目检查:视觉复核样例页与评论区双主题截图;仅在组件骨架或发布链变更时升级范围
|
||||||
|
- 升级触发:只有改动 Artalk 集成骨架或发布链时,才升级验证范围并记录理由
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 样例文章三媒齐渲染(断言 video/audio/img 元素存在) -> verify: task_0004
|
||||||
|
- [x] 评论区双主题截图过视觉审 -> verify: task_0006
|
||||||
|
- [x] E2E 30/30 保持全绿 -> verify: task_0006
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 大视频入仓风险 → 指南纪律 + photos-ingest 类体积闸(后续可加 media-ingest)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- boundary/verify 齐备;verify 只读断言
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 灯箱原图/下载按钮美化
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 只改 GalleryPage.astro 灯箱 lb-orig 区(升级为与 lbnav 同语言的胶囊按钮:图标+文字+悬停态)
|
||||||
|
- verify: `pnpm astro build && grep -q "lb-orig" dist/gallery/index.html`
|
||||||
|
|
||||||
|
- [x] T2 Artalk 评论区美化
|
||||||
|
- claim: op-gitea(mesh 车道,预约 src/components/ArtalkComments.astro)
|
||||||
|
- boundary: ArtalkComments.astro 内注入页面级覆盖样式(.atk-* 选择器,用 tokens 变量,明暗双态随 [data-theme]);可调研 Artalk darkMode 配置项并在 init 传参;不改 Artalk 服务端
|
||||||
|
- verify: `pnpm astro build && grep -q "atk-" src/components/ArtalkComments.astro`
|
||||||
|
|
||||||
|
- [x] T3 markdown 媒体支持
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/styles/global.css 增媒体样式段(.prose-reading/.prose-tech 内 img/figure/video/audio:宽度/圆角/居中/图注样式);验证 raw HTML 透传与图片优化
|
||||||
|
- verify: `pnpm astro build`
|
||||||
|
|
||||||
|
- [x] T4 三媒样张
|
||||||
|
- claim: op-artalk(mesh 车道,预约 content/posts/media-sample.md 与其资产目录)
|
||||||
|
- boundary: ffmpeg 生成 3s 测试视频(<1MB)与 3s 音频;一张测试图;写一篇 draft:true 的样例文章(category: tech, draft 置 true 不上线仅构建验证)三媒齐用;不碰既有六篇
|
||||||
|
- verify: `pnpm astro build && grep -q "<video" dist/archives/*/index.html -r | head -1`
|
||||||
|
|
||||||
|
- [x] T5 写作指南
|
||||||
|
- depends-on: task_0003
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 新建 docs/写作指南.md(三媒插入语法/资产目录约定 content/posts/assets/{slug}/)/体积纪律(图≤1MB 压缩后、音≤5MB、视频>5MB 走仓外 store+绝对 /media/ 路径待 build.sh 同步)
|
||||||
|
- verify: `test -s docs/写作指南.md`
|
||||||
|
|
||||||
|
- [x] T6 验证收官
|
||||||
|
- depends-on: task_0001, task_0002, task_0004, task_0005
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: E2E 复跑 + 样例页与评论区双主题截图交视觉模型 + 归档推送
|
||||||
|
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前进度**:由脚本计算,无需手动维护
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 397 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: "媒体支持样例"
|
||||||
|
pubDate: 2026-10-25
|
||||||
|
category: tech
|
||||||
|
tags: [test, hidden]
|
||||||
|
# 视觉/管线测试样张:draft:true 会被 getStaticPaths 全链路过滤(无静态产物),
|
||||||
|
# 故用非 draft + hidden 标记保证 /archives/media-sample/ 产物存在;视觉测试完成后应删除本篇
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
本篇为三媒管线样张:验证 markdown 图片、HTML5 视频/音频在构建链路中的处理,供视觉回归与多端适配测试使用(hidden 标记,勿收录)。
|
||||||
|
|
||||||
|
## 图片(markdown 语法,构建期经 sharp 优化)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*图注写法:斜体单独成段,photos 目录样片副本(1600×2400 竖幅)。*
|
||||||
|
|
||||||
|
## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成)
|
||||||
|
|
||||||
|
<!-- 视/音频走 public/media/ 绝对路径:raw HTML 相对引用不经打包器(曾实测 404),
|
||||||
|
与写作指南“大视频走仓外 store 的 /media/ 命名空间”对齐 -->
|
||||||
|
|
||||||
|
<video controls preload="metadata" width="320" height="240" src="/media/media-sample/sample-video.mp4"></video>
|
||||||
|
|
||||||
|
*原生 `<video>` 控件,无 JS;源文件 45KB,faststart 已开启。*
|
||||||
|
|
||||||
|
## 音频(raw HTML,440Hz 正弦波 mp3)
|
||||||
|
|
||||||
|
<audio controls preload="metadata" src="/media/media-sample/sample-audio.mp3"></audio>
|
||||||
|
|
||||||
|
*原生 `<audio>` 控件;源文件 18KB。*
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 写作指南:在文章里插入图片 / 视频 / 音频
|
||||||
|
|
||||||
|
> 适用:`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
|
||||||
|

|
||||||
|
*图:外滩的傍晚* ← 紧跟媒体的斜体独立段 = 图注(居中小字)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 图注可选;写法固定为 `*…*` 单独成段,紧跟图片
|
||||||
|
- 压缩纪律:入仓前长边 ≤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 文章,仅构建验证用,不上线)。
|
||||||
|
|
||||||
|
## 发布提醒
|
||||||
|
|
||||||
|
- push main → webhook 自动构建发布(约 10 秒)
|
||||||
|
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
|
||||||
Binary file not shown.
Binary file not shown.
@@ -10,15 +10,86 @@ const { pageKey, pageTitle } = Astro.props;
|
|||||||
|
|
||||||
<div id="artalk-container" data-page-key={pageKey} data-page-title={pageTitle}></div>
|
<div id="artalk-container" data-page-key={pageKey} data-page-title={pageTitle}></div>
|
||||||
|
|
||||||
|
<!-- Artalk 主题接管:--at-color-* 全套映射到站点 tokens(值均为 var(),明暗随
|
||||||
|
html[data-theme] 翻转 tokens 自动生效,一套规则双态成立)。
|
||||||
|
依据:Artalk.css(v2.10) 全部表面由这 18 个变量驱动;init 锁 darkMode:false
|
||||||
|
防 Artalk 自带 atk-dark-mode 类与接管层打架。弹层挂 body 下需单独同款映射 -->
|
||||||
|
<style is:global>
|
||||||
|
#artalk-container,
|
||||||
|
/* Artalk.css 运行时才挂 head,平级选择器后来居上:body 前缀提一级特异性保住覆盖 */
|
||||||
|
body .atk-layer-wrap {
|
||||||
|
--at-color-bg: var(--bg);
|
||||||
|
--at-color-bg-transl: color-mix(in oklab, var(--bg) 95%, transparent);
|
||||||
|
--at-color-bg-grey: var(--bg-soft);
|
||||||
|
--at-color-bg-grey-transl: color-mix(in oklab, var(--bg-soft) 95%, transparent);
|
||||||
|
--at-color-bg-light: var(--accent-soft);
|
||||||
|
--at-color-border: color-mix(in oklab, var(--ink-4) 55%, transparent); /* --line 在白底近不可见(视觉终审建议项),加深一档 */
|
||||||
|
--at-color-font: var(--ink-2);
|
||||||
|
--at-color-deep: var(--ink-1);
|
||||||
|
--at-color-sub: var(--ink-3);
|
||||||
|
--at-color-grey: var(--ink-3);
|
||||||
|
--at-color-meta: var(--ink-4);
|
||||||
|
--at-color-light: var(--ink-4);
|
||||||
|
--at-color-main: var(--accent);
|
||||||
|
/* 高度限高渐隐遮罩必须与 bg 同源,否则暗色下露白腿 */
|
||||||
|
--at-color-gradient: linear-gradient(180deg, transparent, var(--bg));
|
||||||
|
/* red/pink/yellow/green 语义功能色保留官方默认:纪律只管气质,不损语义 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 气质对齐(不动排版):字体、头像圆角、时间戳 mono、链接纪律 */
|
||||||
|
#artalk-container {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
#artalk-container .atk-avatar img {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
#artalk-container .atk-meta,
|
||||||
|
#artalk-container .atk-ua {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
#artalk-container .atk-copyright {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
}
|
||||||
|
#artalk-container .atk-textarea,
|
||||||
|
#artalk-container .atk-input,
|
||||||
|
#artalk-container .atk-send-btn,
|
||||||
|
#artalk-container .atk-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
#artalk-container .atk-content a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: color-mix(in oklab, var(--accent) 40%, transparent);
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
#artalk-container .atk-content a:hover {
|
||||||
|
text-decoration-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const el = document.getElementById('artalk-container');
|
const el = document.getElementById('artalk-container');
|
||||||
if (el) {
|
if (el) {
|
||||||
const mount = () => {
|
const mount = () => {
|
||||||
if (document.getElementById('artalk-lib')) return initArtalk();
|
if (document.getElementById('artalk-lib')) return initArtalk();
|
||||||
|
// 官方部署文档要求 CSS 手动引入(JS 不自动注入);此前缺失导致评论区自上线起跑在 UA 默认样式上
|
||||||
|
const cssReady = new Promise<void>((done) => {
|
||||||
|
if (document.getElementById('artalk-css')) return done();
|
||||||
|
const l = document.createElement('link');
|
||||||
|
l.id = 'artalk-css';
|
||||||
|
l.rel = 'stylesheet';
|
||||||
|
l.href = 'https://wait4x.online/artalk/dist/Artalk.css';
|
||||||
|
l.onload = () => done();
|
||||||
|
l.onerror = () => done(); // CSS 失败不阻塞:评论至少可用(降级 UA 样式)
|
||||||
|
document.head.appendChild(l);
|
||||||
|
});
|
||||||
const s = document.createElement('script');
|
const s = document.createElement('script');
|
||||||
s.id = 'artalk-lib';
|
s.id = 'artalk-lib';
|
||||||
s.src = 'https://wait4x.online/artalk/dist/Artalk.js';
|
s.src = 'https://wait4x.online/artalk/dist/Artalk.js';
|
||||||
s.onload = initArtalk;
|
s.onload = () => { void cssReady.then(initArtalk); };
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
};
|
};
|
||||||
const initArtalk = () => {
|
const initArtalk = () => {
|
||||||
@@ -30,6 +101,8 @@ const { pageKey, pageTitle } = Astro.props;
|
|||||||
pageKey: el.dataset.pageKey,
|
pageKey: el.dataset.pageKey,
|
||||||
pageTitle: el.dataset.pageTitle,
|
pageTitle: el.dataset.pageTitle,
|
||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
|
// 锁亮皮:明暗由页面 tokens 接管(见上方 --at-* 映射),避免双暗色机制叠加
|
||||||
|
darkMode: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// 进入视口 ~200px 前挂载;无 IO 环境直接挂
|
// 进入视口 ~200px 前挂载;无 IO 环境直接挂
|
||||||
|
|||||||
@@ -133,8 +133,14 @@ const first = cards[0];
|
|||||||
<span id="lb-exif">{first?.exif}</span>
|
<span id="lb-exif">{first?.exif}</span>
|
||||||
<span id="lb-count">1 / {cards.length}</span>
|
<span id="lb-count">1 / {cards.length}</span>
|
||||||
<span class="lb-orig">
|
<span class="lb-orig">
|
||||||
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">{t('gallery_lightbox_original', locale)}</a>
|
<a id="lb-original" class="lb-act" href={first?.original} target="_blank" rel="noopener">
|
||||||
<a id="lb-download" href={first?.original} download>{t('gallery_lightbox_download', locale)}</a>
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M21 3l-9 9M10 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-4"></path></svg>
|
||||||
|
{t('gallery_lightbox_original', locale)}
|
||||||
|
</a>
|
||||||
|
<a id="lb-download" class="lb-act" href={first?.original} download>
|
||||||
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 21h16"></path></svg>
|
||||||
|
{t('gallery_lightbox_download', locale)}
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="lb-next" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_next', locale)}>
|
<button id="lb-next" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_next', locale)}>
|
||||||
@@ -357,12 +363,26 @@ const first = cards[0];
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
margin-left: var(--space-2);
|
margin-left: var(--space-2);
|
||||||
}
|
}
|
||||||
.lb-orig a {
|
/* 动作胶囊:与 lbnav 同语言(暗底/描边/阴影),不再是裸链 */
|
||||||
|
.lb-act {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
background: rgba(20, 24, 30, 0.55);
|
||||||
|
color: #dfe6f3;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */
|
text-decoration: none;
|
||||||
text-decoration: underline;
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||||
text-underline-offset: 0.2em;
|
backdrop-filter: blur(4px);
|
||||||
|
transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
|
||||||
|
}
|
||||||
|
.lb-act:hover {
|
||||||
|
background: rgba(40, 48, 60, 0.75);
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
.lb-meta {
|
.lb-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -403,19 +423,8 @@ const first = cards[0];
|
|||||||
right: var(--space-3);
|
right: var(--space-3);
|
||||||
}
|
}
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.lb-bottom { flex-wrap: wrap; }
|
.lb-bottom { flex-wrap: wrap; justify-content: center; }
|
||||||
.lb-orig {
|
.lb-orig { margin-left: 0; }
|
||||||
display: inline-flex;
|
|
||||||
gap: var(--space-3);
|
|
||||||
margin-left: var(--space-2);
|
|
||||||
}
|
|
||||||
.lb-orig a {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 0.2em;
|
|
||||||
}
|
|
||||||
.lb-meta { order: 3; flex-basis: 100%; }
|
.lb-meta { order: 3; flex-basis: 100%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -7,13 +7,23 @@ import { trackOf } from '../../content.config';
|
|||||||
|
|
||||||
// URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改)
|
// URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改)
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
// 路径用全量(hidden 样张也要出静态页);prev/next 导航滤 hidden
|
||||||
|
// (真实文章的上下篇不链向样张——op-artalk 车道连带提醒的实现)
|
||||||
|
const all = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
);
|
);
|
||||||
return posts.map((entry, i) => ({
|
const nav = all.filter(({ data }) => !data.tags.includes('hidden'));
|
||||||
params: { slug: entry.id },
|
return all.map((entry) => {
|
||||||
props: { entry, prev: posts[i + 1] ?? null, next: posts[i - 1] ?? null },
|
const i = nav.findIndex((e) => e.id === entry.id);
|
||||||
}));
|
return {
|
||||||
|
params: { slug: entry.id },
|
||||||
|
props: {
|
||||||
|
entry,
|
||||||
|
prev: i >= 0 ? (nav[i + 1] ?? null) : null,
|
||||||
|
next: i > 0 ? (nav[i - 1] ?? null) : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getCollection } from 'astro:content';
|
|||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import PostList from '../../components/PostList.astro';
|
import PostList from '../../components/PostList.astro';
|
||||||
|
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ import AiTranslatedBadge from '../../../components/AiTranslatedBadge.astro';
|
|||||||
import { t } from '../../../i18n/ui';
|
import { t } from '../../../i18n/ui';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const zh = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
// 路径全量(hidden 样张出页);prev/next 滤 hidden(同 zh 轨修正)
|
||||||
|
const all = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
);
|
);
|
||||||
|
const nav = all.filter(({ data }) => !data.tags.includes('hidden'));
|
||||||
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
|
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
|
||||||
return zh.map((entry, i) => ({
|
return all.map((entry) => {
|
||||||
params: { slug: entry.id },
|
const i = nav.findIndex((e) => e.id === entry.id);
|
||||||
props: { entry, enEntry: en.get(entry.id) ?? null, prev: zh[i + 1] ?? null, next: zh[i - 1] ?? null },
|
return {
|
||||||
}));
|
params: { slug: entry.id },
|
||||||
|
props: {
|
||||||
|
entry,
|
||||||
|
enEntry: en.get(entry.id) ?? null,
|
||||||
|
prev: i >= 0 ? (nav[i + 1] ?? null) : null,
|
||||||
|
next: i > 0 ? (nav[i - 1] ?? null) : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ProjectWall from '../../components/geek/ProjectWall.astro';
|
|||||||
import Heatmap from '../../components/geek/Heatmap.astro';
|
import Heatmap from '../../components/geek/Heatmap.astro';
|
||||||
import { t } from '../../i18n/ui';
|
import { t } from '../../i18n/ui';
|
||||||
|
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
);
|
);
|
||||||
// 列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)
|
// 列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { APIContext } from 'astro';
|
|||||||
// 英文 RSS:postsEn 全量;标题缺译时回退 zh 标题(与 /en/ 页同策略)
|
// 英文 RSS:postsEn 全量;标题缺译时回退 zh 标题(与 /en/ 页同策略)
|
||||||
export async function GET(context: APIContext) {
|
export async function GET(context: APIContext) {
|
||||||
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
|
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
);
|
);
|
||||||
return rss({
|
return rss({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ProjectWall from '../components/geek/ProjectWall.astro';
|
|||||||
import Heatmap from '../components/geek/Heatmap.astro';
|
import Heatmap from '../components/geek/Heatmap.astro';
|
||||||
import { t } from '../i18n/ui';
|
import { t } from '../i18n/ui';
|
||||||
|
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
);
|
);
|
||||||
// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
|
// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { APIContext } from 'astro';
|
|||||||
|
|
||||||
// RSS:posts 集合全量(draft 排除);Phase 3 起双语各出一路(/rss.xml 与 /en/rss.xml)
|
// RSS:posts 集合全量(draft 排除);Phase 3 起双语各出一路(/rss.xml 与 /en/rss.xml)
|
||||||
export async function GET(context: APIContext) {
|
export async function GET(context: APIContext) {
|
||||||
const posts = (await getCollection('posts', ({ data }) => !data.draft))
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden')))
|
||||||
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||||
return rss({
|
return rss({
|
||||||
title: 'wait4x',
|
title: 'wait4x',
|
||||||
|
|||||||
@@ -137,6 +137,40 @@ pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|||||||
.term-dots { display: flex; gap: 6px; margin-bottom: var(--space-2); }
|
.term-dots { display: flex; gap: 6px; margin-bottom: var(--space-2); }
|
||||||
.term-dots span { width: 10px; height: 10px; border-radius: 50%; background: #30363d; }
|
.term-dots span { width: 10px; height: 10px; border-radius: 50%; background: #30363d; }
|
||||||
|
|
||||||
|
/* ---------- 正文媒体(图/视频/音频,Phase 5 媒体支持) ---------- */
|
||||||
|
/* 图片:markdown 相对路径经 Astro 图片服务优化;版心内圆角限宽 */
|
||||||
|
.prose-reading img, .prose-tech img {
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-block: 1.2em;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
/* 图注约定:紧跟媒体的 *斜体独立段* 视为图注(写作指南 docs/写作指南.md) */
|
||||||
|
.prose-reading p > em:only-child, .prose-tech p > em:only-child {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--ink-3);
|
||||||
|
margin-top: -0.6em;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
/* 视频与音频:原生标签,markdown raw HTML 直书(指南有模板) */
|
||||||
|
.prose-reading video, .prose-tech video,
|
||||||
|
.prose-reading audio, .prose-tech audio {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-block: 1.2em;
|
||||||
|
}
|
||||||
|
.prose-reading video, .prose-tech video {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #000;
|
||||||
|
max-height: 72dvh;
|
||||||
|
}
|
||||||
|
.prose-reading audio, .prose-tech audio {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 列表/分隔 ---------- */
|
/* ---------- 列表/分隔 ---------- */
|
||||||
hr { border: none; border-top: 1px solid var(--line); margin-block: var(--space-4); }
|
hr { border: none; border-top: 1px solid var(--line); margin-block: var(--space-4); }
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user