1. 目录总览
只显示主要目录结构,忽略基础内容:
citrus-grid/
├── ......
├── public/
│ ├── avatar.webp
│ ├── logo.webp
│ ├── favicon.ico
│ ├── friends/ # 友链图标目录
│ └── music/ # 音乐播放器音频文件夹
│
├── scripts/ # 构建期 Node 脚本(详见 06-构建与脚本.md)
│ ├── new-post.js
│ ├── format-post-meta.js
│ ├── ensure-post-slugs.js
│ ├── update-post-updated.js
│ ├── generate-lqips.js
│ ├── check-i18n.js # 校验 zh / ja / en 语言包键集合一致(pnpm check-i18n)
│ ├── check-swup-ignore.js # 校验 shouldIgnoreSwup 内联正则与 ASSET_EXT_RE / RESERVED_PATH_RE 常量逐字一致(pnpm check-swup-ignore)
│ └── postbuild.js
│
├── .generated/ # 构建期自动生成的派生数据(已 gitignore)
│ ├── post-index.json # [{ file, slug, draft }]
│ ├── lqips.json # { 图片URL: "632d1b6a301b582816" } 18 位无分隔符十六进制
│ └── post-updated.json # updated 字段同步状态(mtime 快照)
│
└── src/
├── site.config.ts # 站点唯一配置入口
├── content.config.ts # 文章集合 schema(Zod)
├── env.d.ts # 环境类型补充
├── layouts/
│ └── BaseLayout.astro # 全站唯一布局:<head> SEO、主题初始化、常驻 UI
│
├── components/ # 通用组件(14 个)
│ ├── Icon.astro
│ ├── SiteHeader.astro
│ ├── SiteFooter.astro
│ ├── SearchModal.astro
│ ├── ScrollControls.astro
│ ├── ProfileCard.astro
│ ├── DigitalClock.astro # 名片内的数码管时钟:视图 + 走时 / 刷漆切换逻辑
│ ├── PostCard.astro
│ ├── Pagination.astro
│ ├── Comments.astro
│ ├── MusicPlayer.astro
│ ├── Pet.astro # 左下角宠物挂件:视线跟随 / 眨眼 / 休眠 / 滑出 / 长按关闭气泡
│ ├── SeriesToc.astro # 文章页系列目录折叠面板
│ ├── PostNav.astro # 文章页上一篇 / 下一篇
│ └── views/ # 页面级组件(8 个,被 src/pages/ 薄封装)
│ ├── HomePage.astro
│ ├── PostPage.astro
│ ├── ArchivePage.astro
│ ├── TaxonomyPage.astro # 分类总览 / 标签云(kind 区分)
│ ├── SeriesIndexPage.astro
│ ├── SeriesDetailPage.astro
│ ├── FriendsPage.astro
│ └── AboutPage.astro # 关于页,自行修改
│
├── pages/ # 路由(文件即路由,16 个)
├── i18n/ # i18n 目录,包含多语言文件
├── lib/ # 纯逻辑
│ ├── audioCover.ts # 音乐封面处理函数
│ ├── posts.ts # 文章查询、排序、统计、分组、系列
│ ├── icons.ts # MDI 图标硬编码表
│ ├── friends.ts # 友链图标处理函数
│ ├── contentImages.ts # 文章图片处理函数
│ ├── pageLifecycle.ts # 页面生命周期处理函数
│ ├── pointerPreview.ts # 卡片指针预览方窗(attachPointerPreview,名片 / 文章卡共用)
│ ├── siteConfigGuard.ts # 站点配置兜底校验(validateSiteConfig)
│ ├── photoswipe.ts # 文章页灯箱初始化(含按需注入 CSS)
│ ├── postActions.ts # 代码块复制按钮、页内锚点平滑滚动
│ ├── postContent.ts # 正文图片框载入态、滚动淡入
│ ├── postToc.ts # 文章页目录(侧栏 + 移动端弹窗)定位与当前项跟踪
│ ├── player.ts # 音乐播放状态机与纯逻辑(MediaPort 端口)
│ ├── musicUi.ts # 音乐播放器 DOM 助手(元素查询 / 视图更新 / 面板 / 定位)
│ ├── musicPlaylist.ts # 构建期扫描 public/music/ 生成播放列表
│ ├── url.ts # URL 与语言规范(localePath / getLangFromUrl / Swup ignore 判定)
│ ├── client/ # BaseLayout 外置的客户端绑定(入口 index.ts)
│ │ ├── index.ts # 按既有顺序执行下列初始化
│ │ ├── pageBindings.ts # 主题初始化 / TOC 悬浮按钮 / 右下按钮组 / 控件点击委托
│ │ ├── swupBindings.ts # 导航高亮、换页前后钩子、跨页锚点、宠物换页重测
│ │ ├── profileCardAnim.ts # 首页名片进出场动画
│ │ ├── gridTrail.ts # 鼠标划过网格轨迹
│ │ └── pet.ts # 左下角宠物:视线跟随 / 眨眼 / 休眠 / 滑出 / 24 小时休息与唤回
│ └── md/ # 自定义 markdown 插件(8 个:含 vfile 工具)
│
├── styles/ # 全局 + 组件级 CSS(10 个)
├── stores/ # theme.ts / ui.ts(localStorage 状态)
├── comments/ # 评论插件目录(client.ts + utterances.ts)
├── stats/ # 统计插件目录(client.ts + visitor-stats.ts + random-visitor.ts)
├── middleware/index.ts # dev 下给 HTML 加 no-store
└── content/
├── posts/*.md # 文章就在这里
└── images/ # 文章图片(就近引用,见 05 文档)
2. 技术栈与职责划分
2.1 运行时依赖(dependencies)
| 依赖 | 版本 | 在项目里干什么 | 关键引用位置 |
|---|---|---|---|
astro | 7.2.9 | 静态站点框架本体 | 全局 |
@astrojs/markdown-remark | ^7.2.4 | 导出 unified,用于在 astro.config.mjs 里组装 markdown 处理器 | astro.config.mjs |
@astrojs/sitemap | ^3.7.3 | 生成 sitemap-index.xml | astro.config.mjs |
@astrojs/rss | ^4.0.19 | 生成 rss.xml | src/pages/rss.xml.ts |
@swup/astro | ^1.8.0 | 无刷新换页(SPA 式导航 + 预取) | astro.config.mjs |
@tailwindcss/vite | ^4.3.3 | Tailwind v4 的 Vite 插件(CSS-first,无 tailwind.config.js) | astro.config.mjs |
lightningcss | ^1.33.0 | CSS 压缩器(Vite cssMinify) | astro.config.mjs |
unified + remark-* + rehype-* | v11 / v4 系列 | Markdown 解析与转换链 | astro.config.mjs、src/lib/md/* |
katex / rehype-katex / remark-math | ^0.18.4 等 | 数学公式渲染 | PostPage.astro 引 CSS |
prismjs | ^1.30.0 | 构建期代码高亮 | src/lib/md/rehypeCodeBlock.ts |
reading-time | ^1.5.0 | 字数 / 阅读时长统计(CJK 逐字计) | src/lib/posts.ts |
dayjs(含 utc 插件) | ^1.11.23 | 日期格式化(dayjs.utc() 固定按 UTC 输出) | src/lib/posts.ts |
photoswipe | ^5.4.4 | 文章图片灯箱(动态 import) | PostPage.astro |
pagefind | ^1.5.2 | 站内搜索索引与检索 | scripts/postbuild.js、SearchModal.astro |
pinyin-pro | ^3.29.3 | 中文标题自动转拼音 slug | scripts/ensure-post-slugs.js |
2.2 开发依赖(devDependencies)
| 依赖 | 用途 |
|---|---|
@astrojs/check + typescript | pnpm check / pnpm type-check |
@biomejs/biome | 格式化与 lint |
esbuild | postbuild.js 压缩内联脚本 |
sharp | generate-lqips.js 采样图片主色(2×2 缩放取色) |
@types/hast @types/mdast @types/unist @types/prismjs @types/node | 类型声明 |
为什么没有
tailwind.config.js?Tailwind v4 采用 CSS-first 配置。主题扩展写在
src/styles/main.css的@theme inline { ... }块中,暗色变体用@custom-variant dark (&:where(.dark, .dark *))声明。新增设计 token 请加在
main.css,不要试图新建 tailwind 配置文件。
3. 构建流水线(按执行顺序)
┌─ astro build ───────────────────────────────────────────────────────
│
│ ① Vite 插件 buildStart 钩子(astro.config.mjs → vite.plugins)
│ ├─ ensurePostSlugs() 扫描 md,补全缺失的 slug,写 .generated/post-index.json
│ │
│ ├─ generateLqips() 扫描 md 中的图片,sharp 取 3 点主色,写 .generated/lqips.json(增量,LQIP_FORCE=1 全量)
│ │
│ └─ updatePostUpdated() 按文件 mtime 回写 frontmatter 的 updated(仅当 siteConfig.autoUpdatePostUpdated)
│
│
│ ② 内容集合加载(src/content.config.ts 的 Zod schema 校验 frontmatter)
│
│ ③ 页面渲染(src/pages/**)
│ ├─ Markdown → HTML:remark 链 + 自定义 rehype 链(见 05 文档)
│ └─ 全站预渲染为 dist/**/*.html(output 默认 static)
│
│ ④ Vite 打包:esbuild 压缩 JS、lightningcss 压缩 CSS、资源哈希
│
└────────────────────────────────────────────────────────────────
│
▼
┌─ node scripts/postbuild.js ──────────────────────────────────────────────
│ ├─ 并行启动 pagefind --site dist(生成 /pagefind/ 搜索索引)
│ ├─ 遍历 dist/**/*.html:压缩所有「内联且无 src」的 JS(esbuild)
│ └─ 读取每个 HTML 里 module script 引用的 chunk,解析 __vite__mapDeps,把依赖 chunk 以 <link rel="modulepreload"> 注入 <head>
└────────────────────────────────────────────────────────────────
流水线的副作用:构建会改写 src/content/posts/ 里面的 markdown 文件(补 slug、写 updated)。这是有意为之,但意味着构建后 git status 可能有变动,建议先 pnpm build 再 git commit。
4. 数据流:一篇文章从文件到页面
src/content/posts/示例.md
│
│ frontmatter 经 content.config.ts 的 Zod schema 校验 / 归一化
│ (category 取数组首项;tags 支持 "a,b" 字符串或数组,去重去空)
▼
CollectionEntry<'posts'> ──► src/lib/posts.ts
│ ├─ getPublishedPosts() 生产环境过滤 draft,排序:index 降序 → published 降序
│ ├─ wordCount() / readMinutes()(400 字/分钟,最少 1 分钟)
│ ├─ excerptOf() description 优先,否则正文前 120 字
│ └─ groupByCategory / groupByTags / groupByYear
▼
src/pages/*.astro ──(props)──► src/components/views/*.astro
│ │
│ └─ PostCard / ArchivePage 等消费
▼
BaseLayout.astro
├─ 组装 <title> / OG / Twitter Card / canonical / JSON-LD
├─ 内联「防闪烁」主题脚本(读取 localStorage.themeMode)
└─ 渲染常驻 UI:SiteHeader、slot、SiteFooter、SearchModal、ScrollControls、MusicPlayer、Pet
排序规则(很重要,改首页顺序时看这里)
src/lib/posts.ts 的 getPublishedPosts():
posts.sort((a, b) =>
b.data.index - a.data.index || // 先按置顶权重降序
b.data.published.valueOf() - a.data.published.valueOf() // 再按发布时间降序
)
即 index > 0 的文章按数值越大越靠前,index === 0 的文章按时间倒序。
5. 常驻 UI 与 Swup 容器边界
Swup 只替换 #swup-container 的内容(astro.config.mjs 的 containers: ['#swup-container'])。BaseLayout.astro 的结构:
<div class="flex min-h-screen flex-col">
<SiteHeader /> <!-- 常驻,不重建 -->
<div id="swup-container" class="transition-swup-fade flex flex-1 flex-col">
<main class="wp-glass ..."> <!-- ★ 只有这里被替换 -->
<slot />
</main>
</div>
<SiteFooter /> <!-- 常驻 -->
<SearchModal /> <!-- 常驻 -->
<ScrollControls /> <!-- 注释明确说明:放在容器外,避免切页抖动 -->
<MusicPlayer /> <!-- 注释明确说明:放在容器外,保证切页不停止播放 -->
<Pet /> <!-- 装饰性挂件,放在容器外:换页不重建,靠 swup 钩子重测锚点 -->
</div>
二次开发提示:
如果你想新增一个「切页时不重建」的元素(如自定义悬浮挂件、在线客服),放进
#swup-container之外,并在astro.config.mjs的swup.ignore回调里确认相关资源不会被 Swup 拦截。
Swup 的 ignore 规则(astro.config.mjs)会跳过以下 URL:
| 规则 | 说明 |
|---|---|
含 # | 锚点链接跳过 Swup:同页锚点(标题锚点、目录)由内容区平滑滚动;跨页锚点由 BaseLayout 点击委托接管为 SPA 导航 + 平滑滚动(见 07-页面与路由.md#84) |
扩展名为 xml/txt/json/pdf/zip/png/jpg/jpeg/webp/gif/svg/ico/avif/woff/woff2/ttf/eot | 静态资源,不做 SPA 导航 |
路径以 /pagefind/、/fonts/、/images/ 开头 | 索引与资源目录 |
6. 路径别名与代码规范
tsconfig.json:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
"paths": { "@/*": ["src/*"] }
}
}
组件内统一用 @/ 引用(@/components/Icon.astro、@/lib/posts、@/site.config),不要写 ../../ 相对深层路径。
biome.json 关键规则:
| 项 | 值 |
|---|---|
| 缩进 | 2 空格 |
| 行宽 | 100 |
| 引号 | 单引号 |
| 分号 | asNeeded(不写多余分号) |
| Lint 预设 | recommended |
.astro 覆盖 | 关闭 noUnusedVariables / noUnusedImports |
| CSS 解析 | 开启 tailwindDirectives(识别 @theme、@custom-variant) |
7. .generated/ 派生数据说明
| 文件 | 生成者 | 结构 | 消费者 |
|---|---|---|---|
post-index.json | scripts/ensure-post-slugs.js | [{ "file": "posts/示例.md", "slug": "shi-li", "draft": false }] | src/lib/md/rehypeRelativeLinks.ts(把 ./其他.md 转成 /posts/其他-slug/) |
lqips.json | scripts/generate-lqips.js | { "/content-images/883d9fc6f4cc516e.webp": "632d1b6a301b582816" }(3 点主色直接拼接,18 位无分隔符十六进制) | src/lib/md/rehypeImages.ts(生成 --img-lqip 渐变) |
post-updated.json | scripts/update-post-updated.js | { "示例.md": { "mtime": 1730000000000, "stamp": "2026-01-01 00:00:00" } } | 同脚本,用于判断文件是否在同步后又被保存 |
这三个文件丢了会怎样?
删掉后重新构建即可自动重建。
lqips.json丢失只会让首屏图片占位从「主色渐变」退化为「灰白 shimmer」,不影响构建成功。