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

18 KiB
Raw Blame History

时之刃的小站

个人博客源码 —— 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>/,构建时自动扫描目录,不需要在配置里逐个列出。

新增一个相册 = 两件事:

  1. 在 src/data/gallery.ts 的 albums 里加一项(id 要和目录名一致)
  2. 建目录 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.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 分形噪声,不需要任何图片文件:

--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: 前缀是桌面恢复值:

<!-- 正文:手机 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 到树莓派和云服务器:

./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 须知

如果你要基于这个仓库搭自己的博客,至少改这几处:

  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,主题为 VermilionVoid。

许可

代码部分可自由参考取用。src/content/ 下的文章内容版权归作者所有,转载请注明出处。