交互(默认完全看不出可点,保持留白): - 悬停/聚焦:整句话由墨黑转朱红 + 朱红细下划线,署名随之淡入 - 点那句话 → 直接去豆瓣人物页 - 点署名 → 展开一张极简作者卡片,卡片里也能去豆瓣 结构上的几个考量: - 用 <h1><a> 而不是把 h1 变成链接:保住标题语义,点击热区只包住文字 而不是整行 1024px(点旁边空白也跳走会很怪) - 光标 ::after 挂在 h1 上(链接外面),点那个闪烁的竖线不会触发跳转 - 署名占一行固定高度(2.25rem)平时透明,hover 才淡入 —— 空间一直留着, 所以展开卡片时下面的按钮不会往上跳 - 卡片是手风琴式展开(max-height 过渡),留在文档流里,不盖住下面的按钮 - @media (hover: none) 下署名常显 —— 否则手机上这个彩蛋永远看不到 - aria-expanded / aria-controls / aria-label + focus-within 可见焦点, 键盘和读屏用户都能用 - prefers-reduced-motion 下关掉所有过渡 出处核实:多个豆瓣《万寿寺》原文摘录页都收录这句,确认为王小波《万寿寺》。 豆瓣链接在 curl 下会跳 sec.douban.com(反爬),真实浏览器可正常打开。 验证:pnpm build 退出码 0;dist/index.html 里豆瓣链接出现 2 次、 h1 内为 <a>、署名与卡片均在、卡片默认 data-open="false"; 产物 CSS 含 16 条 hero-quote 规则(含 @media(hover:none))
时之刃的小站
个人博客源码 —— 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.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) |
|
--codeblock-bg |
oklch(0.945 0.016 84) |
代码块阴槽 #f2ece1,比页面底更深且同色相 |
--codeblock-topbar-bg |
oklch(0.915 0.017 84) |
代码块顶栏 #e8e2d7 |
--codeblock-bg原来(oklch(0.97 0.003 85))是中性灰白,在暖纸底上会变成一块冷白补丁,很跳。改配色时注意:代码块要比--background更深、色相一致,不能只是"接近白"。
浓淡的三档(页面底是 0.964):
| 档 | 块体 | 顶栏 | 与页面底的明度差 |
|---|---|---|---|
| 轻(当前) | 0.945 #f2ece1 |
0.915 #e8e2d7 |
1.06:1 |
| 中 | 0.925 #ece6da |
0.893 #e1dbcf |
1.12:1 |
| 重 | 0.905 #e5dfd3 |
0.873 #dbd4c7 |
1.19:1 |
对比度都还留得很足(正文 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")。
结构:
| 区块 | 内容 | 显示范围 |
|---|---|---|
| 站点信息卡片 | 建站时间 / 已运行 / 文章 / 偶得 / 总字数 / 最近更新,卡片样式沿用侧栏作者卡片 | 全部 |
| 最新文章 | 最近 4 篇(按发布时间倒序,不把置顶提前) | 桌面 |
| 联系 | 社交图标 + 订阅 RSS | 全部 |
| 底栏 | 版权 / 备案 / 主题署名 | 全部 |
两处刻意不放的东西:
- 「导航」栏已删除 —— 它是顶部导航的逐项复制(9 条名字和目标完全一样),手机上白占 312px。桌面端改用「最新文章」,把重复变成发现。不影响 SEO:顶部导航和移动端菜单里的链接在每页 HTML 里都还在。
- 页脚不再放「我的介绍」 —— 侧栏作者卡片已经有了,再放一份是重复。页脚那份的视觉位置让给了站点信息卡片。
「我的介绍 → 关于页」在侧栏作者卡片里(sidebar.tsx):站名和签名那两行包在同一个 <a href="/about/"> 里,不做 hover 装饰。只能包这两行——下面的社交图标本身是 <a>,<a> 不能嵌套 <a>。aria-label 是必须的,否则读屏会把「站名 + 签名」念成链接名。
统计数据在 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 一个组件,靠 view 属性决定渲染哪一块):
| 页面 | 路径 | 内容 | 侧栏数量 |
|---|---|---|---|
| 在读 | /bookshelf/ |
微信读书里没读完的 | 未读完本数 |
| 书籍 | /bookshelf/books/ |
手工书单(books.json)+ 微信读书已读完 |
两者相加 |
顶部导航「书架」指向 /bookshelf/,所以点进来先落在在读页。
两块内容:
1. 最近在读 —— 来自微信读书,构建时抓取,页面本身不发任何请求。
微信读书 Agent Gateway
↓ scripts/fetch-reading.mjs(读 .env 的 WEREAD_API_KEY)
src/data/reading.json
↓ src/lib/reading.ts
src/components/bookshelf-content.astro
- 刷新数据:
pnpm reading(失败会明确报错并 exit 1)。默认抓整架(1 + N 个请求,书架几十本完全够用);想只抓最近动过的几本就pnpm reading --top 12 pnpm build会自动先跑一次抓取,但用--soft模式:微信读书挂了 / Key 过期 / 断网时,保留上一次的reading.json继续构建,不会阻塞部署src/data/reading.json是数据快照,要提交进 git,这样 CI 或新克隆的仓库没 Key 也能构建- Key 申请:https://weread.qq.com/r/weread-skills 扫码,拿到
wrk-开头的串,写进.env的WEREAD_API_KEY(.env已 gitignore)。只在本地构建时用得上 ——deploy.sh是本地构建再 rsync,凭据不会上传服务器;如果另外接了 Vercel / GitHub Actions 构建,要在那边单独配 secret
接口是腾讯官方的 Agent Gateway(POST https://i.weread.qq.com/api/agent/gateway)。几个实测得到的结论,改脚本前值得知道:
- 请求体里
skill_version必填,业务参数平铺在顶层,不能包进params /shelf/sync一次就给全封面/书名/作者/分类/最近阅读时间/跳转链接,只有阅读进度要额外调/book/getprogress,所以请求数 = 1 + Nbook.progress是 0–100 的整数,只有 100 才算读完book.recordReadingTime恒为 0(别用它),但book.readingTime是真数据,累计时长取后者(已存进 JSON 的readingSeconds,卡片暂未展示)book.startReadingTime官方文档没列,但回包里确实有,就是「开始阅读时间」;只有 readUpdateTime 那 26 本里有 8 本没有它(基本都是没怎么打开的书)deepLink实测是普通 https(https://weread.qq.com/book-detail?...),浏览器能直接打开category是文学-外国文学这种「大类-子类」复合串- 时间戳统一按
YYYY-MM-DD展示(官方硬性要求),顺带避开「N 天前」在静态站上会随构建时间变旧
展示逻辑:
| 归到哪 | 条件 |
|---|---|
| 最近在读 | progress < 100(含 0%,标「还没开始」) |
| 书籍 → 「微信读书 · 已读完」分组 | progress === 100 |
两处用的是同一个组件 src/components/reading-card.astro,保证样式统一 —— 横版卡片、封面在左、信息在右,只有底部两行不同:
| 第 1 行 | 第 2 行 | |
|---|---|---|
| 在读 | 进度条 + 百分比 | 最近阅读 X |
| 已读完 | 开始 X | 读完 Y |
「已读完」那一组混在「书籍」区里,但没有书评/简介,所以点击直接跳微信读书而不是弹窗。它们带 data-weread-card 和 data-category="微信读书",所以搜索框和「领域」筛选同样作用得到(领域里会多出一个「微信读书」选项)。过滤掉的:私密阅读(secret === 1)、从没打开过的书;有声书不处理。
2. 书籍 —— 来自 public/data/books.json,由 bookshelf-content.astro 在构建时 import(文件必须存在)。另外两个文件 papers.json / comics.json 对应的漫画、论文分类已经移除,文件保留但不再被引用。
books.json 为空时,统计行(N 本已读 / N 本推荐 / N 个领域)、搜索框、领域和评价筛选条会整块隐藏 —— 一排「0」加一排没东西可筛的按钮纯占地方。往 books.json 加了书它们会自动回来,不用改代码。手工书单和微信读书都已读完都为空时,会显示一句兜底提示。
两个页面共用同一个组件,不会出现样式跑偏:bookshelf-content.astro 收一个 view: "reading" | "books",只是决定渲染哪一块。侧栏那两行是真链接(带 aria-current 和主色高亮表示当前页),不再是页内锚点。
部署
主用 ./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/ 上去会清空线上目录。
两个目标各自怎么服务的
| 目标 | 服务方式 | 地址 |
|---|---|---|
| 云服务器 | 1Panel 管理的 Caddy 容器(1Panel-caddy-meZ7),配置在 /opt/1panel/apps/caddy/caddy/data/conf/Caddyfile,站点根 /srv/astro-blog |
https://tianxuetao.top |
| 树莓派 | Caddy 容器(blog-caddy,--restart unless-stopped),配置在 /home/pi/caddy/Caddyfile,站点根 /home/pi/www/Astro_firefly |
http://192.168.1.87 |
两个都开了压缩(encode zstd gzip)。这个别删:twikoo.min.js 裸传 506 KB、压完 160 KB,主 CSS 231 KB → 41 KB。Caddy 按 Accept-Encoding 自动协商 zstd / gzip。
Pi 上原本没有 web 服务(文件 rsync 过去了但 80 端口没人听),2026-09-15 补了 Caddy 容器。重建命令:
ssh pi@192.168.1.87
mkdir -p /home/pi/caddy/{data,config}
cat > /home/pi/caddy/Caddyfile <<'EOF'
:80 {
root * /srv
encode zstd gzip
file_server
}
EOF
docker run -d --name blog-caddy --restart unless-stopped -p 80:80 \
-v /home/pi/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
-v /home/pi/caddy/data:/data -v /home/pi/caddy/config:/config \
-v /home/pi/www/Astro_firefly:/srv:ro caddy:alpine
云服务器的 Caddyfile 是 1Panel 生成的。手动改完之后 1Panel 那边可能不认识,改前先按它已有的习惯备份(
Caddyfile.bak.<说明>.<时间戳>),改完用docker exec 1Panel-caddy-meZ7 caddy validate --config /etc/caddy/Caddyfile校验再caddy reload——reload 是原子的,配置有错会保留旧配置继续跑,站点不会挂。
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/ 下的文章内容版权归作者所有,转载请注明出处。