1. 目录总览

只显示主要目录结构,忽略基础内容:

text
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
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)

依赖版本在项目里干什么关键引用位置
astro7.2.9静态站点框架本体全局
@astrojs/markdown-remark^7.2.4导出 unified,用于在 astro.config.mjs 里组装 markdown 处理器astro.config.mjs
@astrojs/sitemap^3.7.3生成 sitemap-index.xmlastro.config.mjs
@astrojs/rss^4.0.19生成 rss.xmlsrc/pages/rss.xml.ts
@swup/astro^1.8.0无刷新换页(SPA 式导航 + 预取)astro.config.mjs
@tailwindcss/vite^4.3.3Tailwind v4 的 Vite 插件(CSS-first,无 tailwind.config.js)astro.config.mjs
lightningcss^1.33.0CSS 压缩器(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中文标题自动转拼音 slugscripts/ensure-post-slugs.js

2.2 开发依赖(devDependencies)

依赖用途
@astrojs/check + typescriptpnpm check / pnpm type-check
@biomejs/biome格式化与 lint
esbuildpostbuild.js 压缩内联脚本
sharpgenerate-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. 构建流水线(按执行顺序)

text
1234567891011121314151617181920212223242526
┌─ 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. 数据流:一篇文章从文件到页面

text
12345678910111213141516171819
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():

ts
1234
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 的结构:

html
12345678910111213
<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:

json
12345678
{
  "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.jsonscripts/ensure-post-slugs.js[{ "file": "posts/示例.md", "slug": "shi-li", "draft": false }]src/lib/md/rehypeRelativeLinks.ts(把 ./其他.md 转成 /posts/其他-slug/)
lqips.jsonscripts/generate-lqips.js{ "/content-images/883d9fc6f4cc516e.webp": "632d1b6a301b582816" }(3 点主色直接拼接,18 位无分隔符十六进制)src/lib/md/rehypeImages.ts(生成 --img-lqip 渐变)
post-updated.jsonscripts/update-post-updated.js{ "示例.md": { "mtime": 1730000000000, "stamp": "2026-01-01 00:00:00" } }同脚本,用于判断文件是否在同步后又被保存

这三个文件丢了会怎样?

删掉后重新构建即可自动重建。lqips.json 丢失只会让首屏图片占位从「主色渐变」退化为「灰白 shimmer」,不影响构建成功。