Astro 的 ClientRouter 切页时会清掉 head 里它不认识的 <style>,而 twikoo
的 CSS 正是 twikoo.min.js 运行时 appendChild 到 head 的;脚本本身又只执行
一次(window.twikoo 已存在就跳过),样式不会重新注入。
表现:从一篇带评论的文章点链接进另一篇后,评论区样式整段丢失 ——
markdown 图标被 .twikoo svg{width:100%;height:100%} 撑成 768x1240px、
文件输入框 .tk-input-image 从 display:none 变回可见、.OwO-items 从 flex
塌成 block 竖排成一列;整页刷新后恢复。
修复:把官方 twikoo.css 放进 public/assets/css/(与现有 twikoo.min.js 同为
1.7.20,已核对两个文件都来自同一个 npm 包、md5 一致),在 Comments.astro
里用静态 <link> 引入,排在 twikoo-custom.css 之前。静态 link 在 <body> 里,
切页时会随新页面一起换进来,不再依赖运行时注入。
没有换用官方 twikoo.nocss.js:它是 all 完整版去掉 CSS(997 KB),比
twikoo.min.js(506 KB,CSS 打包在内)大近一倍,而本站服务器没开 gzip,
换过去等于访客多下 491 KB。
验证(headless Chrome 131,禁用缓存,对本地 dist 实测):
- 直接打开文章页:icon 18.75px / fileInput none / .tk-submit 179px
- 文章 -> 文章切页:修复前 768px / inline-block / 2919px;
修复后与直接打开逐像素一致(diff bbox = None,差异像素 0.0000%)
- 连续切页 3 次、切页后展开 OwO 表情面板,均正常
12 KiB
时之刃的小站
个人博客源码 —— tianxuetao.top
一个基于 Astro 的静态博客,主题为 VermilionVoid。除了常规的文章系统,还有碎碎念、读书/论文/漫画书架、学习追踪与加密文章。
站点配置集中在两个文件,改站名或换域名通常只需要动第一个:
| 文件 | 管什么 |
|---|---|
src/data/site.ts |
站名、域名、描述、关键词、主题色、统计分析 |
src/data/profile.ts |
昵称、签名、头像、社交链接 |
评论配置在 src/data/comments.ts。
日常操作(写文章、发偶得、加照片、部署)看 docs/操作指南.md,那份更偏"照着做";本文偏项目结构与原理。
技术栈
| 层 | 选型 |
|---|---|
| 框架 | Astro 5(output: "static" 纯静态) |
| 交互组件 | React 19(islands)+ Radix UI + Tailwind CSS 4 |
| 代码块 | Expressive Code(行号、折叠、语言徽章、自定义复制按钮) |
| 数学公式 | remark-math + KaTeX |
| 站内搜索 | Pagefind(构建时索引) |
| 页面切换 | Swup |
| 部署 | ./deploy.sh(rsync 到树莓派 + 云服务器) |
| 评论系统 | 自托管的 twikoo(见下) |
包管理器是 pnpm。
快速开始
pnpm install
pnpm dev
| 命令 | 作用 |
|---|---|
pnpm dev |
本地开发服务器(默认 http://localhost:4321) |
pnpm build |
构建到 dist/,并跑 Pagefind 建立搜索索引 |
pnpm preview |
本地预览构建产物 |
pnpm check |
Astro 的类型与内容校验(本项目没有单元测试框架,这就是唯一的自动检查) |
pnpm new-thought "..." |
快速创建一条「偶得」(见下) |
项目结构
src/
├── pages/ 路由入口(每个文件对应一个 URL)
├── layouts/ 页面骨架
├── components/ Astro / React 组件,可复用的原子组件放 components/ui/
├── content/ Markdown 内容集合(见下)
├── data/ 站点配置:站点信息、个人信息、相册、评论、友链
├── styles/ 全局 CSS
├── plugins/ 自定义 remark / rehype / Expressive Code 插件
├── lib/ hooks/ 支撑逻辑
public/ 原样拷贝到产物根目录的静态资源
├── assets/images/ 文章配图
├── gallery/<id>/ 相册照片,构建时自动扫描
scripts/ 内容工具脚本
写作
内容集合定义在 src/content/config.ts,共三个:
posts —— 正式文章
放在 src/content/posts/,支持子目录分类。
---
title: 文章标题
published: 2026-08-17
updated: 2026-08-18 # 可选
description: 摘要 # 可选,用于 SEO 和卡片
image: /images/cover.webp # 可选,封面
tags: [Astro, 前端]
category: TechnicalTutorials
pinned: false # 置顶
draft: false # 草稿不会被构建
---
加密文章:给 frontmatter 加上 encrypted: true 和 password: "你的密码",该文章正文会被加密,需输入密码才能阅读。适合半公开的内容。
免责声明:disclaimer 字段可以引用 src/data/disclaimers.ts 里预设的声明文案。
thoughts —— 偶得
短内容,放在 src/content/thoughts/。只有 published 是必填,其余可选。
---
published: 2026-09-14T12:36:47.215Z # 必填。务必带时区,见下方说明
title: 可选标题
location: 张掖 # 可选,显示在日期旁
weather: 晴 27°C # 可选
images: # 可选,public 下的绝对路径
- /gallery/GalleryFav/某图.webp
tags: [生活]
pinned: false # 可选,置顶排在最前
---
用脚本创建更省事:
pnpm new-thought "换了散热,温度降了 15 度。" --location 张掖 --tags 硬件 --pinned
published一定要带时区(2026-09-14T12:36:47Z这种)。写成不带时区的2026-09-14 12:36,YAML 会按 UTC 解析,显示时会偏移。偶得页按src/data/site.ts的timezone渲染,所以带时区的时间才会显示正确。
studies —— 专题
---
title: 主题
status: 在读 # 在读 | 沉淀中 | 暂搁 | 已结
started: 2026-08
field: 领域 # 可选
subtitle: 副标题 # 可选
epigraph: 题记 # 可选
---
相册
照片放在 public/gallery/<相册id>/,构建时自动扫描目录,不需要在配置里逐个列出。
新增一个相册 = 两件事:
- 在
src/data/gallery.ts的albums里加一项(id要和目录名一致) - 建目录
public/gallery/<id>/并放照片
{
id: "GalleryFav",
name: "喜欢的壁纸",
description: "相册描述",
location: "甘肃-张掖",
date: "2026-07-18",
tags: ["壁纸", "收藏"], // 用于索引页筛选
}
封面优先级:cover 字段 > 目录里的 cover.* 文件 > 第一张图。
照片是原样下发的静态资源,不经过 Astro 优化。分辨率高的图建议先压成 WebP 再放进来,否则单页可能几十 MB。
自定义
想把这个博客改成你自己的,主要改这几处:
| 想改什么 | 改哪里 |
|---|---|
| 站名、域名、描述、主题色、统计 | src/data/site.ts |
| 昵称、签名、头像、社交链接 | src/data/profile.ts |
| 相册 | src/data/gallery.ts + public/gallery/ |
| 评论(twikoo) | src/data/comments.ts |
| 友链列表 | src/data/friends.json |
| 首页打字机文案 | src/components/hero.tsx 顶部的 phrases |
| 导航栏、页脚 | src/components/site-header.astro、src/components/footer.tsx |
| 配色与全局样式 | src/styles/global.css |
| 代码块主题 | astro.config.mjs 里 expressiveCode 的 themes |
| 免责声明模板 | src/data/disclaimers.ts |
| 头像、favicon 等静态资源 | public/ |
代码风格:Astro/TS/TSX 一律 2 空格缩进。组件命名 PascalCase 与 kebab-case 混用,跟着所在目录的既有风格走即可。项目没有配 formatter,改动尽量贴合周围代码。
书架数据
书架页的数据来自这三个文件,由 src/components/bookshelf-content.astro 在构建时 import(所以文件必须存在,删掉会导致构建失败):
public/data/books.jsonpublic/data/papers.jsonpublic/data/comics.json
目前都是空的 {"data":{"items":[],"total":0}},所以书架页显示为空。要填充就按同样的结构往 items 里加记录。
部署
主用 ./deploy.sh,构建后 rsync 到树莓派和云服务器:
./deploy.sh # 构建 + 部署到两个目标(覆盖生产前会确认)
./deploy.sh --pi-only # 只推树莓派(内网预览)
./deploy.sh --cloud-only # 只推云服务器
./deploy.sh --dry-run # 只显示 rsync 会做什么
./deploy.sh --skip-build # 不重新构建,推现有 dist/
./deploy.sh -y # 跳过确认
目标地址和 SSH 密钥在脚本顶部的配置区。脚本会在构建失败或产物异常时直接中止——因为推送用的是 rsync --delete,拿一个残缺的 dist/ 上去会清空线上目录。
vercel.json 仍然保留,把它接到 Vercel 也可以。走 Vercel 时需要在项目里配置环境变量 PUBLIC_TWIKOO_ENV_ID(评论后端地址,留空则不渲染评论区);本地开发写进 .env(已 gitignore),参考 .env.example。
评论系统
使用 twikoo,后端为自托管实例。
配置在 src/data/comments.ts:
export const commentConfig = {
twikooEnvId: "https://twikoo.tianxuetao.top",
lang: "zh-CN",
stripTrailingSlash: true,
}
twikooEnvId 留空则整个评论区不渲染;也可以用环境变量 PUBLIC_TWIKOO_ENV_ID 覆盖,优先级更高。
前端脚本不走 CDN,而是自托管的 public/assets/js/twikoo.min.js。国内访问 jsDelivr 很不稳定(实测 3 秒以上,且常被墙),走 CDN 会让评论区长时间空白甚至加载不出来。该文件的版本要与后端保持一致(当前都是 1.7.20,可用 GET_FUNC_VERSION 查后端版本),升级后端时记得同步替换它。
样式分两层,两个文件必须放在 src/components/Comments.astro 的静态 <link> 里:
| 文件 | 作用 |
|---|---|
public/assets/css/twikoo.css |
官方原样,从 npm 的 twikoo@<版本>/dist/twikoo.css 取,不要手改 |
public/assets/css/twikoo-custom.css |
主题适配层,颜色引用主题变量,自动跟随亮/暗色 |
twikoo.css 必须是静态 <link>,不能只靠 twikoo.min.js 运行时注入。脚本会把样式 appendChild 到 <head>,而 ClientRouter 切页时会清掉 head 里它不认识的 <style>;脚本本身又只执行一次(window.twikoo 已存在就跳过),样式不会重新注入。症状是从一篇带评论的文章点进另一篇后,评论区排版全崩——markdown 图标被撑成几百像素、文件输入框露出来、表情面板塌成一列,整页刷新才恢复。静态 <link> 放在 <body> 里,切页时会随新页面一起换进来,因此不受影响。
twikoo-custom.css 的选择器统一加了 #twikoo 前缀,保证压过官方样式。
升级 twikoo 时,
twikoo.min.js和twikoo.css要一起换,都从同一个 npm 版本的dist/里取。 顺带一提:官方twikoo.nocss.js是 all 完整版去掉 CSS(997 KB),比twikoo.min.js(506 KB,CSS 打包在内)大近一倍,本站服务器又没开 gzip,所以没有采用它。
前端逻辑在 src/components/Comments.astro,处理了两件事:
- 切页重新挂载:本站启用了 Astro 的
ClientRouter。注意astro:page-load在首屏也会触发,所以用dataset.mounted标记防止重复初始化;切页后是新容器、没有该标记,会正常重新挂载。 - 评论归属路径:
trailingSlash是"always",但评论 key 统一去掉结尾斜杠。浏览器提交的是百分号编码过的location.pathname(如/posts/%E7%94%9F%E6%B4%BB/...),后端按原样存储和匹配 —— 所以去斜杠后的编码路径正好与历史数据一致。改掉这条规则,老评论就会对不上。
评论出现在:文章页、关于页、友链页。
主题原作者的自托管评论后端 cwd-api/ 已随迁移移除。
Fork 须知
如果你要基于这个仓库搭自己的博客,至少改这几处:
src/data/site.ts—— 站名、域名、描述、主题色,以及analytics里的统计 ID(每一项留空就不加载对应脚本)。astro.config.mjs的site已经引用这里的site.url,不用单独改。src/data/profile.ts—— 昵称、签名、头像、社交链接。src/data/comments.ts—— twikoo 后端地址,填你自己部署的实例。src/data/friends.json—— 友链列表。
另外这几处仍是从主题原作者仓库带过来的内容,按需清空替换:
src/content/下的文章、碎碎念、学习专题public/data/*.json的书架数据public/images/vote/的赞赏收款码(关于页已不再引用)
致谢
文章系统的部分设计参考了 Fuwari,主题为 VermilionVoid。
许可
代码部分可自由参考取用。src/content/ 下的文章内容版权归作者所有,转载请注明出处。