{"ok":true,"data":[{"file":"my-test.md","slug":"my-test","title":"富名王","summary":"测试","date":"2026-10-26","tags":["随笔","自恋"],"category":"随笔","cover":"","featured":true,"draft":false,"readingMinutes":1,"wordCount":35,"size":236,"body":"# 明富今天荣获国家奖学金一等奖，准备给同学朋友们请喝瑞幸的新品泰奶珍珠奶茶"},{"file":"guoyu-2-rebuild.md","slug":"guoyu-2-rebuild","title":"果语 2.0 从 Vue 2 到 Vite + React 19 的重构手记","summary":"旧站在 Spring Boot 上跑了半年，如今决定把它推倒重来。这篇文章记录了技术选型的取舍、迁移映射，以及一个个人博客到底需不需要后端。","date":"2026-10-13","tags":["重构","Vite","React","架构"],"category":"技术笔记","cover":"/images/covers/cover-rebuild.svg","featured":true,"draft":false,"readingMinutes":3,"wordCount":853,"size":4373,"body":"旧站叫葱舍，Vue 2 + Spring Boot + MySQL 的全栈组合（也就是果语 1.0）。它跑得很稳，但我在写新文章的时候总要面对一个问题：**为了写一篇随笔，我得先启动一个数据库。**\n\n这次重构的目标其实只有一句话——把「发布一篇文章」的成本，从「启动三个服务」压缩到「新建一个文件」。\n\n## 先说结论\n\n新站砍掉了后端。内容以 Markdown 文件的形式躺在 `content/posts/` 里，构建时被打包进静态资源。\n\n```text\n果语 2.0 技术栈\n────────────────────────────────────────\n构建      Vite 7          冷启动 300ms 以内\n框架      React 19 + TypeScript\n样式      Tailwind CSS 3.4 + 设计令牌\n组件      shadcn/ui（源码进仓，不锁定）\n路由      react-router-dom\n内容      Markdown + 自研 frontmatter 解析\n部署      静态托管（dist/ 即全部）\n```\n\n## 为什么不是 Next.js\n\n如果你只想做 SEO 优先的内容站，Next.js 的 SSG 是标准答案。我没有选它，原因很具体：\n\n1. **个人博客是单作者场景**，RSC 的服务端能力大部分用不上；\n2. **构建体积可控**，`dist/` 是一堆静态文件，`base: '/'` 丢到任何托管都能跑；\n3. **心智负担低**，从 Vue 2 迁过来时，我需要的是「一个我完全掌控的构建产物」，而不是一层框架抽象。\n\nSEO 的短板我用别的方式补：每路由独立 metadata、构建期生成 `sitemap.xml` 与 `feed.xml`、文章页注入 `BlogPosting` 结构化数据。这三件事加起来，成本大约一百行代码。\n\n> 架构没有标准答案，只有适不适合。个人站最重要的指标是「三个月后我还愿意维护它吗」。\n\n## 迁移映射表\n\n| 旧站 1.0（Vue 2） | 果语 2.0（Vite + React） |\n|---|---|\n| `views/Home.vue` | `pages/Home.tsx` + `PostCard` |\n| `views/Docs.vue` + `ArticleSearch` | `pages/Posts.tsx` + `⌘K` 命令面板 |\n| `views/ArticleDetail.vue` | `pages/PostDetail.tsx` + TOC + Markdown |\n| `views/Album.vue` / `AlbumDetail.vue` | `pages/Album.tsx` + `MasonryGrid` + `Lightbox` |\n| `views/Settings.vue` | `ThemeToggle`（light / dark / system） |\n| MySQL 文章表 | `content/posts/*.md` |\n| `components/HeatMap.vue` | P2：贡献热力图 |\n| `components/MusicPlayer.vue` | P2：音乐播放器 |\n\n### 一个典型的构建配置\n\n```ts\n// vite.config.ts\nimport path from \"path\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  base: \"/\",\n  plugins: [react()],\n  resolve: {\n    alias: { \"@\": path.resolve(__dirname, \"./src\") },\n  },\n});\n```\n\n内容层用 `import.meta.glob` 在构建期静态收集所有文章，不需要任何后端接口：\n\n```ts\nconst modules = import.meta.glob(\"../../content/posts/*.md\", {\n  query: \"?raw\",\n  import: \"default\",\n  eager: true,\n}) as Record<string, string>;\n```\n\n## 保留了什么\n\n重构不等于推倒一切。旧站有几个设计我原封不动地搬了过来：\n\n- **个人信息卡**（头像 / 名言 / 社交 / 点击复制）——它是我这个站的辨识度所在；\n- **歌词栏与音乐播放器**——列入 P2，等主题系统稳定后再做；\n- **相册的分类概念**（风景 / 美食 / 随心 / 回忆），以及真实的照片资产；\n- **Footer 的运行时长**，一个很朴素但让人愿意常来的小细节。\n\n而被砍掉的是：JWT 鉴权、自建后台、私信、天气与 LeetCode 小组件。它们依赖后端和第三方接口，对一个博客来说收益极低。\n（后来它们又以更轻的形式回来了：一个零依赖的 Node 后台、同源 `/api/*` 代理——但那是后话，主体的通路始终只有 `.md` 文件。）\n\n## 接下来\n\n- [x] 设计令牌与暗色模式\n- [x] Markdown 内容层与阅读体验\n- [ ] 接入 Giscus 评论\n- [ ] 部署到静态托管并配置自定义域名\n"},{"file":"design-tokens-dark-mode.md","slug":"design-tokens-dark-mode","title":"设计令牌与暗色模式 一套不闪烁的主题系统","summary":"暗色模式做得好不好，取决于首屏那 0.2 秒。这篇笔记把果语的色彩令牌、三态主题与无闪烁预设脚本完整记录下来。","date":"2026-10-10","tags":["设计系统","暗色模式","CSS变量"],"category":"技术笔记","cover":"/images/covers/cover-design.svg","featured":true,"draft":false,"readingMinutes":2,"wordCount":764,"size":4096,"body":"\n暗色模式做得好不好，取决于首屏那 0.2 秒。刷新页面时如果看到一瞬间的白屏闪烁，用户会立刻觉得「这个站不专业」。\n\n## 一、令牌先行：组件不写死颜色\n\n果语的全部颜色定义在 `:root` 和 `.dark` 两个作用域里，Tailwind 负责把它们映射成语义类名。\n\n```css\n:root {\n  --background: 75 30% 98%;\n  --foreground: 100 20% 12%;\n  --primary: 96 44% 27%;\n  --primary-foreground: 84 50% 96%;\n  --border: 84 18% 87%;\n}\n\n.dark {\n  --background: 100 14% 6.5%;\n  --foreground: 85 18% 92%;\n  --primary: 84 45% 55%;\n  --border: 90 10% 19%;\n}\n```\n\n```ts\n// tailwind.config.js\ncolors: {\n  background: \"hsl(var(--background))\",\n  foreground: \"hsl(var(--foreground))\",\n  primary: {\n    DEFAULT: \"hsl(var(--primary))\",\n    foreground: \"hsl(var(--primary-foreground))\",\n  },\n}\n```\n\n组件里只写 `bg-background`、`text-muted-foreground`、`border-border`。这样切换主题时**不需要给任何组件加 `.dark` 前缀**，变量一变，全站跟着变。\n\n### 为什么主色在两套主题里亮度不同\n\n浅色模式下主色是 `hsl(96 44% 27%)`（深果绿），深色模式提亮到 `hsl(84 45% 55%)`（亮果绿）。原因是同样的色相，亮度和背景的对比关系完全相反：\n\n| 模式 | 主色 | 背景 | 正文对比度 |\n|---|---|---|---|\n| 浅色 | 深果绿 27% | 暖纸白 | 6.6 : 1 |\n| 深色 | 亮果绿 55% | 深夜果园 | 9.0 : 1 |\n\n> 暗色不是「反色」，而是独立调校的第二套色彩。\n\n### 插画专用令牌\n\n除语义令牌外，还有一组 `--avocado-*` 专给牛油果插画用（果皮 / 果肉 / 果核 / 嫩叶），\n让线稿 logo 之外的彩色插画也有唯一的取色出处：\n\n```css\n:root {\n  --avocado-skin: 96 44% 27%;   /* 果皮 */\n  --avocado-flesh: 82 52% 82%;  /* 果肉 */\n  --avocado-pit: 28 42% 36%;    /* 果核 */\n  --avocado-leaf: 100 45% 38%;  /* 嫩叶 */\n}\n```\n\n## 二、三态主题：浅色 / 深色 / 跟随系统\n\n```ts\ntype Theme = \"light\" | \"dark\" | \"system\";\n```\n\n用户选择持久化在 `localStorage`；选 `system` 时监听 `prefers-color-scheme` 的变化事件，系统切换主题网站跟着切。\n\n## 三、首屏无闪烁：在 CSS 之前同步执行\n\n关键在于：这段脚本必须在样式表生效**之前**跑完，阻塞在 `<head>` 里。\n\n```html\n<script>\n  (function () {\n    try {\n      var stored = localStorage.getItem(\"guoyu-theme\");\n      var systemDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n      var dark = stored ? stored === \"dark\" : systemDark;\n      document.documentElement.classList.toggle(\"dark\", dark);\n      document.documentElement.style.colorScheme = dark ? \"dark\" : \"light\";\n    } catch (e) {}\n  })();\n</script>\n```\n\n有了它，React 挂载之前 `html` 上就已经有了正确的 class。用户完全看不到中间态。\n\n## 四、别忘了可访问性\n\n暗色模式最容易翻车的地方是**边框和次要文字**——它们在深色背景上如果直接取反，会变成一片噪点。我的处理方式：\n\n1. 正文对比度 ≥ 4.5:1，次要文字 ≥ 4.5:1，两套主题都实测；\n2. 深色模式弱化阴影，改用 `border + 亮度差` 分层；\n3. 所有动效在 `prefers-reduced-motion: reduce` 下统一降级：\n\n```css\n@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    transition-duration: 0.01ms !important;\n  }\n}\n```\n\n## 验收清单\n\n- [x] 刷新页面无主题闪烁\n- [x] 三态切换后刷新仍保持\n- [x] 系统切换主题时网站实时跟随\n- [x] 两套主题下正文对比度均 ≥ 4.5:1\n- [x] 全站颜色可追溯到令牌，无「临时起意」的第四种颜色\n"},{"file":"git-as-cms.md","slug":"git-as-cms","title":"Git 即 CMS 把博客内容变成 Markdown 文件","summary":"不装数据库，不写后台。文章就是仓库里的一个 .md 文件，提交即发布，回滚就是 git revert。","date":"2026-10-06","tags":["内容管理","Markdown","工作流"],"category":"技术笔记","cover":"/images/covers/cover-workflow.svg","featured":true,"draft":false,"readingMinutes":2,"wordCount":750,"size":3629,"body":"\n我曾经为了写一篇两百字的随笔，需要做这些事：启动 MySQL、启动 Spring Boot、启动前端、打开后台、粘贴内容、点发布。\n\n现在我只需要做一件事：在 `content/posts/` 里新建一个 `.md` 文件。\n\n## frontmatter 就是元信息\n\n```md\n---\ntitle: Git 即 CMS 把博客内容变成 Markdown 文件\nsummary: 不装数据库，不写后台。文章就是仓库里的一个 .md 文件。\ndate: 2026-10-06\ntags: 内容管理, Markdown, 工作流\ncategory: 技术笔记\ncover: /images/covers/cover-workflow.svg\nfeatured: true\ndraft: false\n---\n\n正文从这里开始……\n```\n\n字段约定：\n\n| 字段 | 必填 | 说明 |\n|---|---|---|\n| `title` | 是 | 文章标题，同时用于 SEO 的 `<title>` |\n| `summary` | 是 | 摘要，列表页与 OG description 共用 |\n| `date` | 是 | `yyyy-mm-dd`，倒序排列的依据 |\n| `tags` | 否 | 逗号分隔，用于标签聚合页 |\n| `category` | 是 | 一级分类，对应旧站的「专栏」 |\n| `cover` | 否 | 封面图，建议 16:9 |\n| `featured` | 否 | `true` 时进首页精选区 |\n| `draft` | 否 | `true` 时仅开发环境可见，生产构建自动剔除 |\n\n## 构建期读取，而不是运行时请求\n\n```ts\n// src/lib/posts.ts\nconst modules = import.meta.glob(\"../../content/posts/*.md\", {\n  query: \"?raw\",\n  import: \"default\",\n  eager: true,\n}) as Record<string, string>;\n```\n\n`eager: true` 意味着这些文件在**打包时**就被读进内存。运行时的博客不会发出任何网络请求，也就不存在接口挂掉的风险。\n\n## 顺手解决的三件事\n\n### 1. 草稿保护\n\n```ts\n.filter((p) => !p.draft || import.meta.env.DEV)\n```\n\n写了没想好？设 `draft: true`，本地能看，线上不出现。不用再手动清理。\n\n### 2. 阅读时长\n\n中文按 400 字/分钟、英文按 200 词/分钟估算，比一律按 300 字符/分钟更贴近中文读者：\n\n```ts\nconst cjk = (body.match(/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/g) || []).length;\nconst en = (body.replace(/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/g, \" \")\n  .match(/[a-zA-Z0-9]+/g) || []).length;\nconst minutes = Math.max(1, Math.round(cjk / 400 + en / 200));\n```\n\n### 3. 目录（TOC）\n\n文章页右侧目录不是运行时扫描 DOM 的，而是构建期从正文里正则提取 `##` / `###`：\n\n```ts\nconst re = /^(#{2,3})\\s+(.+)$/gm;\n```\n\n这样目录和正文标题天然一致，不会出现「点目录跳不到」的情况。\n\n## 发布流程\n\n```bash\n# 新建文章 → 写内容\nvim content/posts/new-post.md\n\n# 本地预览\nnpm run dev\n\n# 提交即上线\ngit add content/posts/new-post.md\ngit commit -m \"post: 新文章标题\"\ngit push\n```\n\n托管平台监听 push，自动执行 `npm run build`，几十秒后文章就出现在线上了。整个过程没有数据库迁移、没有缓存刷新、没有重启服务。\n\n> 个人博客的内容量，几十年后也就几百篇 Markdown。文件是最耐久的数据格式。\n\n## 一点取舍\n\n这套方案的代价是**没有评论、没有浏览量、没有全文搜索的服务端能力**。所以评论我打算用 Giscus（数据留在 GitHub Discussions），浏览量用轻量服务或干脆不做。\n\n如果哪天我需要一个真正的后台，说明这个博客已经不是个人博客了——到那时再换架构也不迟。\n"},{"file":"vue2-to-react19.md","slug":"vue2-to-react19","title":"从 Vue 2 到 React 19 思维方式的三个转弯","summary":"响应式、模板编译、组件通信——这三个概念在 React 里都有对应的替代品，但真正需要换掉的是写代码时脑子里的顺序。","date":"2026-09-28","tags":["React","Vue","思维"],"category":"技术笔记","cover":"/images/covers/cover-rebuild.svg","featured":false,"draft":false,"readingMinutes":2,"wordCount":664,"size":3031,"body":"\n写 Vue 2 的时候，我几乎不需要想「数据变了界面会怎么变」。`this.xxx = 1` 之后，所有用到它的地方自动更新。这种「不用管」的舒适，是我这次重构最大的适应成本。\n\n## 第一个转弯 从「双向绑定」到「单向数据流」\n\nVue 里最顺手的是 `v-model`，双向绑定的代价是**你不太确定谁是真相之源**。React 把这个问题直接解决掉：状态只能通过 setter 改变，界面永远由状态推导。\n\n```jsx\n// ❌ 曾经的直觉：直接改\ninputRef.current.value = \"果语\";\n\n// ✅ React 的方式：改数据，界面自己跟上\nconst [name, setName] = useState(\"\");\nonChange={(e) => setName(e.target.value)}\n```\n\n一开始我很不适应，因为多了一步。但换来的是**任何一个 UI 状态都能回答「它从哪来」**——这在果语这种多组件共享主题的站点里非常值钱。\n\n## 第二个转弯 从「模板」到「函数」\n\nVue 模板是编译产物，调试时你会看到生成代码；React 里模板就是函数，调试栈与源码一一对应。这是我最喜欢的变化之一：报错信息直接指向我写的那一行。\n\n```jsx\n{posts.map((post) => (\n  <PostCard key={post.slug} post={post} />\n))}\n```\n\n代价是要自己记住 key、ref 规则这些纪律。但换来的可读性提升，在文章列表这种场景里立竿见影。\n\n## 第三个转弯 从「全局状态」到「就近状态」\n\nVuex 的思路是：既然状态难管理，那就集中起来。React 生态走了另一条路——**状态默认放在需要它的地方，只有真正跨组件才提升或用 Context**。\n\n果语里的主题就是一个好例子。它确实要跨全站，但我没有上任何状态库：\n\n```tsx\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport function useTheme() {\n  const ctx = useContext(ThemeContext);\n  if (!ctx) throw new Error(\"useTheme 必须在 ThemeProvider 内使用\");\n  return ctx;\n}\n```\n\n三十行代码解决的事，用 Redux 加中间件大概要八十行。**先别引入你不需要的东西**，这是这次迁移学到的最重要的一件事。\n\n## 一点体会\n\n> 框架的迁移不是 API 的翻译，而是工作流的迁移。真正的效率提升，来自把「自动」换成了「显式」。\n\nVue 2 是我 2024 年入门的工具，它的价值是把复杂的状态管理藏起来让我先跑起来。React 19 则是让我看清了那些被藏起来的部分。两者没有高下，只是阶段不同。\n\n如果你是从 Vue 转 React，最难的不是 hooks，而是**忍住不用 ref 直接改 DOM**。熬过那一周，剩下的都是水到渠成。\n"},{"file":"photo-migration.md","slug":"photo-migration","title":"23 张旧照片的一次搬家","summary":"旧站 assets/images/Camera 下躺着 23 张照片、约 20MB。这次只搬走其中 11 张，顺手把相册页做成能键盘操作的灯箱。","date":"2026-09-20","tags":["摄影","性能","资产整理"],"category":"生活随笔","cover":"/images/covers/cover-photo.svg","featured":false,"draft":false,"readingMinutes":2,"wordCount":603,"size":3101,"body":"\n翻旧项目的时候，发现 `frontend/src/assets/images/Camera/` 里静静躺着五个文件夹：\n\n```text\nfengjing/   风景 10 张   6.2M\nmeishi/     美食  5 张   3.1M\nsuixin/     随心  4 张   1.8M\nhuiyi/      回忆  4 张   1.5M\nzhuanti/    专题  4 张   7.9M\n```\n\n`zhuanti` 里是游戏和影视的封面图，那是旧站「实时热点」轮播的素材，不属于我自己的照片，先不动。其余四类，我挑了 11 张搬进新站。\n\n## 为什么不全搬\n\n理由很朴素：**博客不会展示全部照片**。\n\n- 单张图片动辄 1.6MB，一次打开 10 张就是十几 MB；\n- 分类命名还带着 `fengjing (1).jpg` 这种形态，需要重命名；\n- 手机竖构图和横构图混排，需要给瀑布流预设宽高比，避免布局抖动。\n\n所以搬的时候顺手做了这几件事：\n\n1. 重命名为语义化文件名（`fengjing-1.jpg` 而不是 `fengjing (1).jpg`）；\n2. 保留原图，不做任何二次压缩——原件是回忆，不该被工具改写；\n3. 给每张照片写一句标题和描述，让相册在数据层面可被检索。\n\n## 瀑布流的实现比想象中简单\n\n不需要 JavaScript 计算布局。CSS 的多列布局天生就是瀑布流：\n\n```css\n.columns-2 {\n  column-count: 2;\n  column-gap: 1rem;\n}\n\n.masonry-item {\n  break-inside: avoid; /* 关键：不要被列切断 */\n  margin-bottom: 1rem;\n}\n```\n\n```tsx\n<MasonryGrid>\n  {photos.map((photo, index) => (\n    <MasonryItem key={photo.id}>\n      <button onClick={() => setOpenIndex(index)}>\n        <img src={photo.src} alt={photo.title} loading=\"lazy\" />\n      </button>\n    </MasonryItem>\n  ))}\n</MasonryGrid>\n```\n\n## 灯箱要照顾键盘\n\n相册页最容易被忽略的无障碍细节，是**它能不能只用键盘看完**。我用 Radix 的 Dialog 做基础，自己补了方向键：\n\n```tsx\nuseEffect(() => {\n  if (!open) return;\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"ArrowLeft\") prev();\n    if (e.key === \"ArrowRight\") next();\n  };\n  window.addEventListener(\"keydown\", onKeyDown);\n  return () => window.removeEventListener(\"keydown\", onKeyDown);\n}, [open, prev, next]);\n```\n\n这样，Tab 进入相册、点开第一张之后，`←` `→` 可以翻到任何一张，`Esc` 退出。整个过程不需要碰鼠标。\n\n## 关于图片优化\n\n目前图片是原图 + `loading=\"lazy\"`。下一步要做的是：\n\n- 用 `sharp` 在构建期批量转 WebP，体积能降 60% 左右；\n- 给首屏之外的图片加 `blurDataURL` 占位；\n- 灯箱里的图走全尺寸，列表页只发缩略图。\n\n> 技术债不总是坏事。知道欠在哪里、什么时候还，比一开始全都做对更重要。\n\n照片搬完了，还差 12 张没找齐。慢慢来，反正这个站有的是时间。\n"},{"file":"soup-and-code.md","slug":"soup-and-code","title":"代码写不动的时候 需要一碗热汤","summary":"葱姜蒜下锅的声音，是程序员的重启键。这篇不讲技术，只讲我怎么用一碗汤把卡住的三小时熬过去。","date":"2026-09-12","tags":["美食","生活"],"category":"生活随笔","cover":"/images/covers/cover-food.svg","featured":false,"draft":false,"readingMinutes":1,"wordCount":383,"size":1713,"body":"\n昨天下午写主题切换逻辑，卡在同一个地方整整三小时：变量改了，组件没更新；组件更新了，首屏又闪一下。越修越乱，最后我把电脑合上，去了厨房。\n\n## 葱姜蒜的顺序\n\n我妈教过我：先姜后蒜，蒜要晚一步。\n\n- 冷油下姜片，小火煸到边缘微卷——这是给底味；\n- 转中火下蒜瓣，出香味就立刻下一步；\n- 别等蒜变黄，变黄就是苦的。\n\n这道汤没有复杂的配料，一块白萝卜、一把虾皮、两滴香油。煮到汤色发白就好了。\n\n## 意外的部分\n\n站在灶台前的那二十分钟里，我脑子里一直在自动回放那段代码。然后忽然意识到——我一直在 `useEffect` 里 `setState`，而问题真正出在**服务端渲染假设**和**首次挂载时序**上，跟 effect 没关系。\n\n回家一改，五分钟就通过了。\n\n> 大脑需要的不是休息，是换一种消耗方式。手上有事的时候，想不清楚的事反而会自己浮上来。\n\n## 顺便说个发现\n\n我把这个博客的美食分类空着了很久，一直觉得「技术博客不应该放吃的」。搬家整理相册时翻到 `meishi` 文件夹，看着那几张图，突然觉得博客就是一个人的全部，凭什么只有代码。\n\n所以这个分类现在有内容了。以后可能会多起来。\n\n汤还在锅里，先去吃一口。\n"},{"file":"hello-guoyu.md","slug":"hello-guoyu","title":"写在果语 2.0 上线这天","summary":"两年零一个月，从 localhost 到一个真的域名。这个博客叫什么、为什么是颗牛油果、接下来往哪去。","date":"2026-09-05","tags":["果语","随笔","牛油果"],"category":"随笔","cover":"/images/covers/cover-hello.svg","featured":true,"draft":false,"readingMinutes":1,"wordCount":540,"size":2364,"body":"\n博客叫 **avocado 果语**。牛油果是绿的、饱满、外糙内润；「语」是果肉之内、果核之中，那些想慢慢说出来的话。\n\n## 为什么是牛油果\n\n我的用户名、邮箱、头像，到处都是牛油果的影子。它最早只是顺手起的名字，后来成了我在互联网上的坐标——看到它就等于看到我。\n\n牛油果也很像写博客这件事：外表是一层不起眼的粗皮，切开才有嫩绿的果肉和一颗笃定的核。**写字就是切开自己的过程**，果核里那点想法，说出来才算数。\n\n而「果语」这两个字，说的是这个博客的语气——不喧哗、不追热点，像一颗挂在枝头慢慢熟的果子。\n\n## 为什么重做一遍\n\n旧站在 2024 年 9 月立项，用的是当时我唯一会的组合：Vue 2 + Spring Boot + MySQL。它教会了我很多——JWT 怎么签、拦截器怎么写、Nginx 怎么反代。\n\n但它也教会我一件更重要的事：**一个博客不该长得像一个小系统**。我花在鉴权和私信上的时间，比花在写字上的还多。\n\n## 这次不一样在哪\n\n不是技术栈更时髦了，而是**内容的成本降到了最低**：\n\n- 写文章 = 新建一个 `.md` 文件；\n- 发布 = `git push`；\n- 回滚 = `git revert`；\n- 没有数据库，没有需要担心的服务。\n\n后来我还是给自己搭了一个轻量后台（`npm run admin`）和一个收私信的小服务——理由不是「博客需要后端」，而是「我想知道有没有人在看」。它们都是可选的点缀，内容的通路始终只有那一个 `.md` 文件。\n\n## 接下来\n\nP0 已经完成：设计系统、内容层、阅读体验、相册、关于页、后台与私信。\n\n往下走：\n\n1. 接入 Giscus 评论，数据留在 GitHub Discussions；\n2. 批量把相册转成 WebP，补上缩略图；\n3. 把旧站 `HeatMap`、`MusicPlayer`、`LyricBar` 这几个我喜欢的组件，用新的方式做一遍。\n\n如果你逛到这里，欢迎留言交换链接。\n\n因为看见，所以遇见。\n"}]}