Files
NewBlog/README.md
T
dsh 0cfcbd060e 页脚改版:删掉重复的导航栏,桌面补站点信息;我的介绍可点去关于页
页脚「导航」栏
- 它是顶部导航的逐项复制(9 条名字和目标完全一样),手机上白占 312px
- 已整栏删除;桌面端改用「最新文章」(最近 4 篇,按发布时间倒序,
  不把置顶提前),把重复变成发现
- 手机端「最新文章」隐藏(上面就是文章列表,再列一遍没意义)
- 不影响 SEO:顶部导航 hidden md:flex 和移动端菜单里的同一批链接
  在每页 HTML 里都还在,爬虫照样抓得到

站点信息(新增)
- 新增 src/lib/site-stats.ts:文章数/偶得数/分类数/标签数/总字数
  在构建期算;「已运行 N 天」「最近更新」由页脚内联脚本按真实时间刷新,
  避免站点久不重建时数字停在构建那天;服务端先渲染构建时兜底值,关 JS 不空
- 页脚加一条站点信息带:建站时间 / 已运行 / 文章 / 偶得 / 总字数 / 最近更新
  桌面 6 列一行,手机 3 列两行
- 实测数值:2026-07-17 建站、已运行 60 天、30 篇文章、8 条偶得、
  2.9 万字、最近更新 59 天前

「我的介绍」跳关于页
- 页脚 BRAND 栏整块包 <a href="/about/">,加 hover 反馈(头像描边转主色、
  站名转主色、右上箭头位移)和常显的「关于我 →」提示(手机没有 hover,
  不能只在 hover 才出现)
- 侧栏作者卡片同样处理。注意这里**只包到签名那一段** —— 下面的社交图标
  本身是 <a>,<a> 不能嵌套 <a>;页脚 BRAND 栏没有嵌套链接,所以可以整块包
- 两处都给 aria-label,避免读屏把「头像 alt + 站名 + 签名」念成链接名

工程改动
- footer.tsx -> footer.astro:React 组件不能在构建期 await 统计数据,
  换成 Astro 组件后可以直接取;12 处引用同步改成默认导入
- README 补「页脚」一节,并修正组件路径

验证(headless Chrome 131,禁用缓存)
- 桌面 1280:页脚 529px,三栏(我的介绍/最新文章/联系)+ 站点信息 + 底栏,
  页脚内 nav 链接数 0,无横向溢出
- 手机 390:页脚 822px(原 957),最新文章 display:none,三栏只剩两栏
- 点页脚「我的介绍」-> /about/;点侧栏作者卡片 -> /about/,均正常
- 页面内 `a a` 嵌套数量:页脚 0、侧栏 0
2026-09-15 09:55:26 +08:00

371 lines
18 KiB
Markdown
Raw 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.
# 时之刃的小站
个人博客源码 —— [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/<id>/ 相册照片,构建时自动扫描
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/<id>/` 并放照片
```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.astro` |
| 配色与全局样式 | `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
<!-- 正文:手机 15.5px / 1.78,≥768px 回到 16.5px / 1.85 -->
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` 是固定页眉的实测高度,移动端菜单和这条胶囊条都靠它定位。**改页眉内边距或头像尺寸时记得同步改它**,否则吸附位置会错。
### 页脚
`src/components/footer.astro`(原来是个 React 组件 `footer.tsx`,为了能在构建期 `await` 统计数据改成了 Astro 组件;引用方式随之从 `import { Footer } from "@/components/footer"` 变成 `import Footer from "@/components/footer.astro"`)。
结构:
| 区块 | 内容 | 显示范围 |
|---|---|---|
| 我的介绍 | 头像 + 站名 + 签名,**整块可点去 `/about/`** | 全部 |
| 最新文章 | 最近 4 篇(按发布时间倒序,不把置顶提前) | 桌面 |
| 联系 | 社交图标 + 订阅 RSS | 全部 |
| 站点信息 | 建站时间 / 已运行 / 文章 / 偶得 / 总字数 / 最近更新 | 全部(想只在桌面显示就给它加 `hidden md:grid`) |
| 底栏 | 版权 / 备案 / 主题署名 | 全部 |
**原来的「导航」栏已删除** —— 它是顶部导航的逐项复制(9 条名字和目标完全一样),手机上白占 312px。桌面端改用「最新文章」,把重复变成发现。不影响 SEO:顶部导航和移动端菜单里的链接在每页 HTML 里都还在。
两点需要注意:
- **「我的介绍」整块包 `<a>` 是合法的**,因为这一块里没有别的链接。但**侧栏的作者卡片不行** —— 它下面有社交图标(本身是 `<a>`),`<a>` 不能嵌套。所以那里只包到签名那一段。
- 整块链接要给 `aria-label`,否则读屏会把「头像 alt + 站名 + 签名」整段念成链接名。
统计数据在 `src/lib/site-stats.ts`:文章数/偶得数/分类数/标签数/总字数在构建时算,「已运行 N 天」和「最近更新」由页脚里的内联脚本按**真实时间**刷新(站点久不重建也不会停在构建那天),服务端会先渲染一份构建时兜底值,关 JS 也不空。加减统计项改 `footer.astro` 里的 `statCells` 数组即可。
### 磨砂玻璃
`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` 的静态 `<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 须知
如果你要基于这个仓库搭自己的博客,至少改这几处:
1. **`src/data/site.ts`** —— 站名、域名、描述、主题色,以及 `analytics` 里的统计 ID(每一项留空就不加载对应脚本)。`astro.config.mjs` 的 `site` 已经引用这里的 `site.url`,不用单独改。
2. **`src/data/profile.ts`** —— 昵称、签名、头像、社交链接。
3. **`src/data/comments.ts`** —— twikoo 后端地址,填你自己部署的实例。
4. **`src/data/friends.json`** —— 友链列表。
另外这几处仍是从主题原作者仓库带过来的内容,按需清空替换:
- `src/content/` 下的文章、碎碎念、学习专题
- `public/data/*.json` 的书架数据
- `public/images/vote/` 的赞赏收款码(关于页已不再引用)
## 致谢
文章系统的部分设计参考了 [Fuwari](https://github.com/saicaca/fuwari),主题为 [VermilionVoid](https://github.com/Lapis0x0/VermilionVoid)。
## 许可
代码部分可自由参考取用。`src/content/` 下的文章内容版权归作者所有,转载请注明出处。