# 时之刃的小站 个人博客源码 —— [tianxuetao.top](https://tianxuetao.top) 一个基于 Astro 的静态博客,主题为 [VermilionVoid](https://github.com/Lapis0x0/VermilionVoid)。除了常规的文章系统,还有碎碎念、读书/论文/漫画书架、学习追踪与加密文章。 站点配置集中在两个文件,改站名或换域名通常只需要动第一个: | 文件 | 管什么 | |---|---| | `src/data/site.ts` | 站名、域名、描述、关键词、主题色、统计分析 | | `src/data/profile.ts` | 昵称、签名、头像、社交链接 | 评论配置在 `src/data/comments.ts`。 > 日常操作(写文章、发偶得、加照片、部署)看 **[docs/操作指南.md](docs/操作指南.md)**,那份更偏"照着做";本文偏项目结构与原理。 --- ## 技术栈 | 层 | 选型 | |---|---| | 框架 | [Astro](https://astro.build) 5(`output: "static"` 纯静态) | | 交互组件 | React 19(islands)+ Radix UI + Tailwind CSS 4 | | 代码块 | Expressive Code(行号、折叠、语言徽章、自定义复制按钮) | | 数学公式 | remark-math + KaTeX | | 站内搜索 | Pagefind(构建时索引) | | 页面切换 | Swup | | 部署 | `./deploy.sh`(rsync 到树莓派 + 云服务器) | | 评论系统 | 自托管的 [twikoo](https://twikoo.js.org)(见下) | 包管理器是 **pnpm**。 ## 快速开始 ```bash pnpm install ``` ```bash 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// 相册照片,构建时自动扫描 scripts/ 内容工具脚本 ``` ## 写作 内容集合定义在 `src/content/config.ts`,共三个: ### `posts` —— 正式文章 放在 `src/content/posts/`,支持子目录分类。 ```yaml --- 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` 是必填,其余可选。 ```yaml --- published: 2026-09-14T12:36:47.215Z # 必填。务必带时区,见下方说明 title: 可选标题 location: 张掖 # 可选,显示在日期旁 weather: 晴 27°C # 可选 images: # 可选,public 下的绝对路径 - /gallery/GalleryFav/某图.webp tags: [生活] pinned: false # 可选,置顶排在最前 --- ``` 用脚本创建更省事: ```bash 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` —— 专题 ```yaml --- title: 主题 status: 在读 # 在读 | 沉淀中 | 暂搁 | 已结 started: 2026-08 field: 领域 # 可选 subtitle: 副标题 # 可选 epigraph: 题记 # 可选 --- ``` ## 相册 照片放在 `public/gallery/<相册id>/`,构建时**自动扫描目录**,不需要在配置里逐个列出。 新增一个相册 = 两件事: 1. 在 `src/data/gallery.ts` 的 `albums` 里加一项(`id` 要和目录名一致) 2. 建目录 `public/gallery//` 并放照片 ```ts { 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,改动尽量贴合周围代码。 ### 浅色配色:暖纸底,不用亮白 浅色模式**刻意不用纯白**。纯白(L\*≈99)在屏幕上偏刺眼,而且大面积纯色没有视觉落点,看久了发死。整套浅色 token 压到 L\*≈96 并给了一点暖黄彩度(oklch hue 84 ≈ 米黄),墨色也换成暖褐而非纯黑: | token | 值 | 说明 | |---|---|---| | `--background` | `oklch(0.964 0.014 84)` | 页面底 `#f8f3e9` | | `--surface-subtle` | `oklch(0.952 0.016 84)` | 区块底 `#f4eee3`,比页面底再深一档 | | `--card` | `oklch(0.98 0.011 84)` | 卡片**比页面底亮**,靠明度差拉出层次 `#fcf8f0` | | `--foreground` | `oklch(0.235 0.013 68)` | 暖墨 `#221d18`,不是 `#000` | | `--muted-foreground` | `oklch(0.505 0.012 70)` | 元信息,对比度 5.3:1 | | `--border` / `--input` | `oklch(0.888 0.016 84)` | | 对比度都还留得很足(正文 15:1、次要文字 7.5:1、主色链接 4.9:1,WCAG AA 要求 4.5:1),所以是「柔和」不是「发灰」。**暗色模式没动。** 调暖 / 调冷就改这几个数字的**色相**(84 = 米黄,70 更偏橙,95 更偏柠檬);想更白就把 L 往上推、彩度往下压。 ### 纸纹底 `--paper-noise` 是一段内联 SVG 的 `feTurbulence` 分形噪声,不需要任何图片文件: ```css --paper-noise: url("data:image/svg+xml,...feTurbulence baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'..."); --paper-noise-opacity: 0.2; /* 暗色 0.22 */ ``` 由 `body::before` 铺成一层 `position: fixed` 的整屏颗粒(`z-index: 1`、`pointer-events: none`)。它有两个作用:给纯色底一点纸的质感;让 `backdrop-filter` 有细节可模糊 —— 背后是纯色时磨砂等于不存在。 调参: - **颗粒粗细**看 `baseFrequency`,越大越细(0.7–0.9 是细砂纸,0.3 会变成明显斑块) - **浓淡**看 `--paper-noise-opacity`。实测颗粒亮度标准差:0.05 → 0.7,0.2 → 1.9,0.3 → 2.9(0 = 完全平整)。2 左右是「看得出是纸、但不觉得脏」的档位 - `stitchTiles='stitch'` 不能删,否则平铺有接缝 - 伪元素用的是**灰阶 alpha 噪点**(`feColorMatrix saturate=0`),所以不需要 `mix-blend-mode`,亮暗通用 ### 移动端字号 移动端(<768px)的字号比桌面端小一档,用 Tailwind 的移动优先写法表达 —— 基础值是手机用的,`md:` 前缀是桌面恢复值: ```html prose-p:text-[15.5px] md:prose-p:text-[16.5px] prose-p:leading-[1.78] md:prose-p:leading-[1.85] ``` 改动集中在 `ArticleContent.astro`(正文、标题、引用、列表)和 `article-list.tsx`(列表区标题)。**不要动 `html` 的根字号**,那会连带缩放所有 rem 间距,容易连锁出问题。 ### 移动端分类胶囊条 手机端侧栏(`sidebar.tsx`)被排在文章列表**下面**(实测第一篇文章 y=999、侧栏 y=3212),分类筛选基本摸不到。所以 `article-list.tsx` 在列表顶部额外渲染了一条吸附胶囊条: - `sticky top-[var(--header-h)]`,跟在固定页眉下面;`-mx-6` 拉通全宽,内部再 `px-6` 对齐正文 - 左端第一个胶囊是当前模式,末尾一个胶囊用来在「分类 / 标签」之间切换 - 两端渐隐提示可以横向滑(靠 `filterEdges` 状态控制显隐),滚动条用 `.no-scrollbar` 藏掉 - **复用同一份** `categories` / `activeCategory` / `handleCategoryChange`,不写第二套逻辑;同时把 `sidebar.tsx` 里那两个块加了 `hidden lg:block`,避免同页出现两份 `--header-h: 69px` 是固定页眉的实测高度,移动端菜单和这条胶囊条都靠它定位。**改页眉内边距或头像尺寸时记得同步改它**,否则吸附位置会错。 ### 磨砂玻璃 `global.css` 里有四个 token: | token | 默认 | 用途 | |---|---|---| | `--glass-bg` | `color-mix(in oklch, var(--card) 72%, transparent)` | 面板底色,由 `--card` 派生,亮/暗自动跟随 | | `--glass-border` | `color-mix(in oklch, var(--border) 75%, transparent)` | 面板描边 | | `--glass-blur` | `14px` | 模糊半径 | | `--glass-saturate` | `1.5` | 饱和度补偿,纯模糊会发灰 | 用法:`background-color: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));` **只给「浮在内容上方、面积不大」的元素用**(移动端菜单面板、页眉、搜索面板)。不要给文章卡片大面积用,一屏十几个元素同时 `backdrop-filter` 会明显掉帧。另外记得带上 `-webkit-` 前缀,并在 `@supports not (backdrop-filter: blur(1px))` 里回退成不透明的 `var(--card)`。 > 移动端页眉目前**故意关掉**了常驻磨砂(`site-header.astro` 的 `@media (max-width: 767px)` 那段):固定元素持续模糊会让移动端合成开销变大。要开的话建议用「滚动后才开」的方式,别让它一直生效。 ## 书架数据 书架页的数据来自这三个文件,由 `src/components/bookshelf-content.astro` 在**构建时 import**(所以文件必须存在,删掉会导致构建失败): - `public/data/books.json` - `public/data/papers.json` - `public/data/comics.json` 目前都是空的 `{"data":{"items":[],"total":0}}`,所以书架页显示为空。要填充就按同样的结构往 `items` 里加记录。 ## 部署 主用 `./deploy.sh`,构建后 rsync 到树莓派和云服务器: ```bash ./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](https://twikoo.js.org),后端为自托管实例。 配置在 `src/data/comments.ts`: ```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` 的静态 `` 里**: | 文件 | 作用 | |---|---| | `public/assets/css/twikoo.css` | 官方原样,从 npm 的 `twikoo@<版本>/dist/twikoo.css` 取,**不要手改** | | `public/assets/css/twikoo-custom.css` | 主题适配层,颜色引用主题变量,自动跟随亮/暗色 | `twikoo.css` 必须是**静态 ``**,不能只靠 `twikoo.min.js` 运行时注入。脚本会把样式 `appendChild` 到 ``,而 `ClientRouter` 切页时会清掉 head 里它不认识的 `