<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>初月_Yuechu 官方主页</title><description>月初时代</description><link>https://www.ycera.cn/</link><language>zh_CN</language><item><title>你好，初月</title><link>https://www.ycera.cn/posts/welcome/</link><guid isPermaLink="true">https://www.ycera.cn/posts/welcome/</guid><description>初月_Yuechu 的个人博客正式开张，这里会记录一些日常与想法。</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;欢迎来到我的新网站。&lt;/p&gt;
&lt;p&gt;这里是初月_Yuechu 的个人博客，一个用来存放文字、记录日常、整理想法的地方。网站刚刚搭好，很多东西还在慢慢添置，但至少从今天开始，它会有一个真正属于自己的角落。&lt;/p&gt;
&lt;h2&gt;关于名字&lt;/h2&gt;
&lt;p&gt;初月，月初的月，月初的初。名字里有「开始」的意思，也希望这里能一直保持刚开始写东西时的那份认真。&lt;/p&gt;
&lt;h2&gt;之后会写什么&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;日常生活的片段与随想&lt;/li&gt;
&lt;li&gt;折腾网站、工具和代码的记录&lt;/li&gt;
&lt;li&gt;读到的好书、看过的作品&lt;/li&gt;
&lt;li&gt;偶尔一些没有归类的碎碎念&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;且视他人之疑目如盏盏鬼火，大胆地去走你的夜路。&lt;/p&gt;
&lt;p&gt;如果你偶然路过这里，欢迎随便看看。之后的故事，就从这一篇开始。&lt;/p&gt;
</content:encoded></item><item><title>从主题到 npm 包：同一份代码的两种活法</title><link>https://www.ycera.cn/posts/theme-to-npm-package/</link><guid isPermaLink="true">https://www.ycera.cn/posts/theme-to-npm-package/</guid><description>同一个博客主题，既可以直接改源码，也可以作为 npm 包安装到别人项目里。双模式带来的约束比想象中多。</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最初我以为博客主题只有一种使用方式：克隆仓库、改配置、写文章。后来才发现，同一份代码还可以发布成 npm 包，让其他人在自己的 Astro 项目里安装使用。&lt;/p&gt;
&lt;p&gt;这两种模式共享同一套源码，但运行环境完全不同。&lt;/p&gt;
&lt;h2&gt;两种模式的分界&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
  Source[主题源码]
  Source --&amp;gt; A[源码模式：项目直接持有 src/]
  Source --&amp;gt; B[npm 模式：用户项目安装 shirones]
  A --&amp;gt; A1[配置在 src/config]
  B --&amp;gt; B1[配置覆盖在用户项目]
  B --&amp;gt; B2[内容在 shirones/content]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;源码模式下，项目拥有所有文件，想改哪里就改哪里。npm 模式下，主题代码在 &lt;code&gt;node_modules&lt;/code&gt; 里，用户的配置和内容必须放在包外的位置。&lt;/p&gt;
&lt;p&gt;==同一个改动，在两种模式下都成立，才算真正完成。==&lt;/p&gt;
&lt;h2&gt;配置不能假设自己在哪&lt;/h2&gt;
&lt;p&gt;源码模式里，读取 &lt;code&gt;src/config/siteConfig.ts&lt;/code&gt; 看起来理所当然。到了 npm 模式，这个路径可能属于主题包，而不是用户项目。&lt;/p&gt;
&lt;p&gt;因此配置读取需要遵循「用户覆盖优先」的规则，而不是直接读主题目录里的默认值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const userConfig = readUserConfig(&quot;site&quot;);
const finalConfig = userConfig
  ? mergeWithDefaults(userConfig)
  : defaults;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[不要在主题代码里随意读 process.cwd()]
&lt;code&gt;process.cwd()&lt;/code&gt; 指向用户项目根目录，不代表主题文件也在那里。读取主题自有文件时，应该基于包自身路径，而不是当前工作目录。
:::&lt;/p&gt;
&lt;h2&gt;构建配置需要重新生成&lt;/h2&gt;
&lt;p&gt;Astro 的集成机制允许主题在启动时重建 &lt;code&gt;astro.config.mjs&lt;/code&gt;，把必需的插件、路由和资源注入用户项目。这意味着配置文件不是静态的，而是会根据模式动态生成。&lt;/p&gt;
&lt;p&gt;:::tip[集成层的职责是「补齐」，不是「覆盖」]
集成应该只补用户没有配置的部分，并保留用户自己的设置。直接覆盖用户的 Astro 配置，会让主题变得难以共存。
:::&lt;/p&gt;
&lt;h2&gt;发布前必须检查依赖边界&lt;/h2&gt;
&lt;p&gt;最容易出问题的地方，是「本地能构建，安装后失败」。原因通常是某个包写在了 &lt;code&gt;devDependencies&lt;/code&gt;，但运行时需要它。&lt;/p&gt;
&lt;p&gt;项目专门有一条检查命令，用来发现这种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm check:package-manifest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会在发布前扫描源码里的 import，确认每个运行时依赖都出现在正确的位置。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;依赖类型&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;是否随包安装&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dependencies&lt;/td&gt;
&lt;td&gt;运行时必需&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;peerDependencies&lt;/td&gt;
&lt;td&gt;由用户项目提供&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;devDependencies&lt;/td&gt;
&lt;td&gt;仅主题仓库开发使用&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::caution[侧效应导入也要留意]
某些包只是在类型声明或副作用文件里被引用，不会被普通打包分析捕捉。检查时需要区分「声明文件」和「运行时代码」，否则容易漏掉真正的缺失依赖。
:::&lt;/p&gt;
&lt;h2&gt;文档也是交付物&lt;/h2&gt;
&lt;p&gt;npm 模式最容易让用户困惑的，不是代码，而是「我的配置应该放哪」「内容目录在哪」「为什么改了没生效」。这些问题需要在文档里给出明确答案，而不是让用户读源码。&lt;/p&gt;
&lt;p&gt;:::details[双模式最累的部分是什么？]
不是写两套代码，而是每次改动都要问：这个改动在另一种模式下还成立吗？路径、配置、资源归属，任何一项假设错了，都会在发布后才暴露。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;从主题到 npm 包，增加的是一层交付责任。源码模式可以只对自己负责，npm 模式必须对陌生环境负责。&lt;/p&gt;
&lt;p&gt;也正因为如此，双模式反而逼着我把路径、配置和依赖边界想清楚。&lt;strong&gt;能经得起别人安装的代码，通常也会让自己的项目更干净&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>零额外负担：可选功能应该怎么设计</title><link>https://www.ycera.cn/posts/zero-extra-burden/</link><guid isPermaLink="true">https://www.ycera.cn/posts/zero-extra-burden/</guid><description>评论、统计、小组件这些功能，开着很好用，关掉却常常留下残留。本站用一个简单原则约束它们。</description><pubDate>Fri, 30 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;博客主题很容易变成「功能堆叠」：评论、统计、音乐、动画、各种小部件。每加一个功能，作者就多一份配置负担，读者就多一份加载成本。&lt;/p&gt;
&lt;p&gt;本站采用一个很朴素的原则：&lt;strong&gt;关掉的功能，必须像从未存在过一样&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;什么叫「零额外负担」&lt;/h2&gt;
&lt;p&gt;一个可选功能在 &lt;code&gt;enable: false&lt;/code&gt; 或完全未配置时，应该满足四个条件：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;要求&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;网络&lt;/td&gt;
&lt;td&gt;零外部请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOM&lt;/td&gt;
&lt;td&gt;零节点、零布局偏移&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;包体&lt;/td&gt;
&lt;td&gt;零新增依赖进入主包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;兼容&lt;/td&gt;
&lt;td&gt;不要求旧文章补字段&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==「关掉」不是「隐藏」，而是根本不参与构建。==&lt;/p&gt;
&lt;h2&gt;配置层先做判断&lt;/h2&gt;
&lt;p&gt;最简单的做法，是在配置解析阶段就把关闭的功能过滤掉，而不是等渲染时再 &lt;code&gt;if&lt;/code&gt; 一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const navBarConfig: NavBarConfig = {
  links: pruneUnavailableNavLinks(defaultLinks, unavailableFeatureRoutes),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样导航、页面入口和站点地图都拿到同一份结果，不会出现「导航没了，但页面还在」的半关闭状态。&lt;/p&gt;
&lt;h2&gt;需要交互的功能按需加载&lt;/h2&gt;
&lt;p&gt;有些功能即使开启，也不应该进入首屏主包。例如评论系统和图表渲染，只有在页面真正需要时才动态加载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[页面构建] --&amp;gt; B{功能开启?}
  B -- 否 --&amp;gt; C[不生成任何资源]
  B -- 是 --&amp;gt; D[生成触发点]
  D --&amp;gt; E{用户触发?}
  E -- 否 --&amp;gt; F[零网络请求]
  E -- 是 --&amp;gt; G[动态加载模块]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[把「关闭」当成默认状态测试]
不要只在功能开启时测试页面。关掉功能后重新构建，检查 DOM、网络请求和包体是否真的干净，才能确认零负担成立。
:::&lt;/p&gt;
&lt;h2&gt;为什么不用「留着以后可能有用」&lt;/h2&gt;
&lt;p&gt;未使用的代码不会自动消失。它会进入依赖树、增加构建时间、带来安全更新负担，还会让后来的人误以为它是必要部分。&lt;/p&gt;
&lt;p&gt;:::warning[功能开关不是垃圾回收]
加一个 &lt;code&gt;enable&lt;/code&gt; 字段很容易，但关掉之后残留的样式、资源和数据读取不会自己消失。每个可选功能都需要明确的生命周期。
:::&lt;/p&gt;
&lt;h2&gt;对内容作者的友好性&lt;/h2&gt;
&lt;p&gt;零负担还有一层含义：&lt;strong&gt;不强迫旧内容迁移&lt;/strong&gt;。新增一个可选字段时，旧文章不需要批量补 frontmatter；字段缺失时应该有合理默认值，而不是构建失败。&lt;/p&gt;
&lt;p&gt;:::details[这条原则从哪里来？]
它来自一次次「只是加个小功能」带来的连锁反应：配置越来越多、页面越来越慢、关掉之后还要清理残留。把约束写进规则，比事后补救更省力。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;可选功能的设计难点，不在开启时的效果，而在关闭时的干净程度。一个功能只有做到「开则可用、关则无痕」，才值得长期留在主题里。&lt;/p&gt;
&lt;p&gt;这也是本站愿意为一些小功能写更多判断逻辑的原因：&lt;strong&gt;复杂度应该由需要它的人承担，而不是由所有人承担&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>搜索、RSS 与站点地图：静态站也要可被发现</title><link>https://www.ycera.cn/posts/search-rss-sitemap/</link><guid isPermaLink="true">https://www.ycera.cn/posts/search-rss-sitemap/</guid><description>页面能打开只是第一步。让别人找得到、订阅得到、抓取得到，才算真正发布。</description><pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;静态博客没有数据库，也没有后台索引服务，但这不代表内容只能靠人记住网址。本站用几种「构建期生成」的方式，让内容可被搜索、订阅和抓取。&lt;/p&gt;
&lt;h2&gt;搜索索引在构建时生成&lt;/h2&gt;
&lt;p&gt;Pagefind 会扫描构建产物里的 HTML，生成一套可以前端查询的索引。整个过程不需要服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm build
npx pagefind --site dist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建完成后，搜索框直接查询本地索引。文章内容变化时，索引也会在下次构建时更新。&lt;/p&gt;
&lt;p&gt;:::note[只索引真正的页面]
搜索索引只收录带 &lt;code&gt;data-pagefind-body&lt;/code&gt; 标记的内容页。归档、标签、关于页这类导航页面不会挤占搜索结果，用户搜到的基本都是文章本身。
:::&lt;/p&gt;
&lt;h2&gt;RSS 和 Atom 是长期订阅的基础&lt;/h2&gt;
&lt;p&gt;RSS 看起来像上个时代的东西，但它仍然是内容订阅最稳定的方式之一。本站同时生成 RSS、Atom 和 JSON 订阅源，读者可以用任意阅读器订阅。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;输出&lt;/th&gt;
&lt;th&gt;路径&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RSS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rss.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通用订阅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atom&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/atom.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标准化订阅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/sitemap-index.xml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;搜索引擎抓取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llms.txt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/llms.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;面向 AI 阅读的站点摘要&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip[订阅源要跟着内容一起构建]
不要手写订阅文件。让构建流程从文章集合生成，既能保证顺序正确，也不会出现「文章更新了，RSS 还是旧的」。
:::&lt;/p&gt;
&lt;h2&gt;关闭的页面不能进站点地图&lt;/h2&gt;
&lt;p&gt;站点地图应该只包含真正可访问的页面。如果某个功能被关闭，页面会跳转到 404，但它仍然可能出现在地图里，造成无效抓取。&lt;/p&gt;
&lt;p&gt;本站用一份统一的禁用页面清单来过滤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (skillsConfig.enable === false) disabled.push(&quot;skills&quot;);
if (projectsConfig.enable === false) disabled.push(&quot;projects&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置和数据只维护一处，页面入口、导航裁剪和站点地图都读同一份状态。&lt;/p&gt;
&lt;p&gt;==站点地图不是「页面清单」，而是「可访问页面清单」。==&lt;/p&gt;
&lt;h2&gt;面向机器阅读的文本&lt;/h2&gt;
&lt;p&gt;除了给搜索引擎看的 sitemap，本站还生成 &lt;code&gt;llms.txt&lt;/code&gt; 和 &lt;code&gt;llms-full.txt&lt;/code&gt;，用纯文本描述站点结构和主要内容。这不是为了排名，而是为了让自动阅读工具更容易理解站点。&lt;/p&gt;
&lt;p&gt;:::details[需要多少 SEO 技巧？]
对个人博客来说，最重要的是标题、描述、可访问的 URL 和稳定的订阅源。复杂的排名技巧收益有限，先把基础做对更重要。
:::&lt;/p&gt;
&lt;h2&gt;发布检查清单&lt;/h2&gt;
&lt;p&gt;:::steps{title=&quot;每次发布后可以检查这几项&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;搜索索引是否更新&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构建日志里应该出现 Pagefind 的索引结果，页面数量与文章数量大致对应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;订阅源是否包含新文章&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;打开 RSS 或 Atom，确认最新一篇出现在列表顶部。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;站点地图是否排除了关闭页面&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;检查 &lt;code&gt;sitemap-index.xml&lt;/code&gt;，确认没有已关闭功能的路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述和标题是否完整&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分享卡片和搜索结果都依赖 frontmatter 里的 &lt;code&gt;title&lt;/code&gt; 与 &lt;code&gt;description&lt;/code&gt;。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;搜索、订阅和站点地图都不是「上线之后再补」的功能。它们和文章一起生成，才能保证内容在发布的那一刻就是可发现的。&lt;/p&gt;
&lt;p&gt;对静态站点来说，这可能是最划算的一类工程投入：构建一次，长期受益。&lt;/p&gt;
</content:encoded></item><item><title>一次字体构建失败带来的教训</title><link>https://www.ycera.cn/posts/local-fonts-lesson/</link><guid isPermaLink="true">https://www.ycera.cn/posts/local-fonts-lesson/</guid><description>构建时突然报 CannotFetchFontFile，页面还没生成就失败了。问题不在代码，而在构建阶段依赖了外部 CDN。</description><pubDate>Wed, 28 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;那天改完配置，执行 &lt;code&gt;pnpm build&lt;/code&gt;，终端直接抛出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[CannotFetchFontFile] Failed to fetch font file
https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地开发一切正常，构建却失败了。原因很明确：Astro 的字体提供方默认会从 CDN 拉取字体文件，而当时网络无法访问那个地址。&lt;/p&gt;
&lt;h2&gt;构建阶段的外部依赖是隐藏风险&lt;/h2&gt;
&lt;p&gt;很多构建流程看起来是「本地执行」，实际上仍然依赖远程资源：字体、图标、封面图、第三方数据。只要其中任何一个不可达，构建就会中断。&lt;/p&gt;
&lt;p&gt;:::caution[能构建成功，不等于能在任何环境构建]
同一份代码，在作者机器上能过，在 CI 或另一台机器上失败，通常就是外部依赖造成的。构建可复现性比构建速度更值得优先保证。
:::&lt;/p&gt;
&lt;h2&gt;把字体放回本地&lt;/h2&gt;
&lt;p&gt;解决方式不是给 CDN 加重试，而是把字体文件放进仓库，改用本地数据源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  id: &quot;outfit-body&quot;,
  family: &quot;Outfit&quot;,
  role: &quot;body&quot;,
  source: &quot;local&quot;,
  variants: [
    { file: &quot;src/assets/fonts/outfit-latin-400-normal.woff2&quot;, weight: 400 },
    { file: &quot;src/assets/fonts/outfit-latin-500-normal.woff2&quot;, weight: 500 },
    { file: &quot;src/assets/fonts/outfit-latin-700-normal.woff2&quot;, weight: 700 },
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改完之后，构建不再请求外部字体地址，失败问题也随之消失。&lt;/p&gt;
&lt;p&gt;:::steps{title=&quot;本地字体的处理流程&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;准备字体文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从依赖包或官方渠道获取 woff2，放进 &lt;code&gt;src/assets/fonts/&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;声明本地来源&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在字体配置里把 &lt;code&gt;source&lt;/code&gt; 改为 &lt;code&gt;local&lt;/code&gt;，并列出每个字重对应的文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;构建时子集化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;构建工具会根据页面实际用到的字符生成子集，减少体积。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;检查预算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行字体检查脚本，确认总大小没有超过约定上限。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;子集化不等于可以随便加字体&lt;/h2&gt;
&lt;p&gt;中文全量字体动辄几 MB，即使子集化，多个字重叠加也会迅速膨胀。本站的字体预算约为 6 MB，实际构建后约 844 KB，仍然有空间，但不能无节制地增加。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;大小&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;西文正文字体&lt;/td&gt;
&lt;td&gt;约 30 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;等宽字体&lt;/td&gt;
&lt;td&gt;约 66 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中文字体子集&lt;/td&gt;
&lt;td&gt;约 749 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==字体优化的第一步不是压缩，而是删掉不需要的字重。==&lt;/p&gt;
&lt;h2&gt;中文字重的现实限制&lt;/h2&gt;
&lt;p&gt;本站使用的中文字体只有一个 Medium 字重。标题需要的 600/700 会由浏览器合成，效果不如真实字重扎实。&lt;/p&gt;
&lt;p&gt;:::note[这是一个已知取舍]
换成带 400/500/700 的中文字体可以彻底解决，但文件体积会明显增加。当前阶段我选择先保证构建稳定和加载速度，把字体升级留到后续。
:::&lt;/p&gt;
&lt;h2&gt;这次故障留下的经验&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;构建流程里尽量不依赖远程资源；&lt;/li&gt;
&lt;li&gt;能本地化的资源就本地化；&lt;/li&gt;
&lt;li&gt;出问题时先看「构建阶段请求了什么」，而不是只盯着报错行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[一个检查习惯]
如果你的构建在同一台机器上偶尔成功、偶尔失败，先检查网络依赖。可复现的构建，才有资格谈持续集成。
:::&lt;/p&gt;
&lt;p&gt;字体问题看起来很小，但它提醒我：&lt;strong&gt;网站能否构建成功，取决于最薄弱的那条外部链路&lt;/strong&gt;。把链路收回来，心里才踏实。&lt;/p&gt;
</content:encoded></item><item><title>i18n 工程化：十种语言，不靠硬编码</title><link>https://www.ycera.cn/posts/i18n-engineering/</link><guid isPermaLink="true">https://www.ycera.cn/posts/i18n-engineering/</guid><description>界面文案看起来只是字符串，但一旦散落在组件里，就会变成最难维护的隐形成本。</description><pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;做多语言最容易犯的错误，是在组件里直接写一句中文，然后想着「以后有空再抽出来」。结果是文案越写越多，最后没人知道哪些已经翻译、哪些还是漏网之鱼。&lt;/p&gt;
&lt;p&gt;本站的做法是从一开始就把界面文案集中管理。&lt;/p&gt;
&lt;h2&gt;一个 Key，十份翻译&lt;/h2&gt;
&lt;p&gt;所有用户可见的文案都通过 &lt;code&gt;I18nKey&lt;/code&gt; 枚举引用，再交给翻译模块解析。组件里不出现具体语言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import I18nKey from &quot;@i18n/i18nKey&quot;;
import { i18n } from &quot;@i18n/translation&quot;;

const title = i18n(I18nKey.archive);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于 &lt;code&gt;archive&lt;/code&gt; 最终显示成「归档」还是「Archive」，由当前语言决定，组件不需要知道。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层级&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I18nKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定义所有可翻译的文案键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语言模块&lt;/td&gt;
&lt;td&gt;提供各语言的翻译值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i18n()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行时查找与回退&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组件&lt;/td&gt;
&lt;td&gt;只引用键，不写具体文案&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;占位符必须同名&lt;/h2&gt;
&lt;p&gt;带参数的文案是最容易出错的地方。例如「共 {count} 篇」在每种语言里都必须保留 &lt;code&gt;{count}&lt;/code&gt; 这个占位符名，否则替换时就会丢值。&lt;/p&gt;
&lt;p&gt;:::warning[翻译不是改写]
参数名属于接口契约，不属于语言表达。可以调整语序、增加量词，但不能把 &lt;code&gt;{count}&lt;/code&gt; 改成 &lt;code&gt;{数量}&lt;/code&gt;，否则代码找不到对应的值。
:::&lt;/p&gt;
&lt;h2&gt;回退策略要明确&lt;/h2&gt;
&lt;p&gt;当某个语言缺少某个键时，系统需要一个确定的回退顺序。本站优先使用当前语言，缺失时回退到默认语言，而不是直接显示键名。&lt;/p&gt;
&lt;p&gt;:::note[键名不应该出现在界面上]
如果用户看到 &lt;code&gt;archive&lt;/code&gt; 这样的英文键，说明回退链断了。构建和测试阶段应该尽量把这类问题暴露出来，而不是留给读者发现。
:::&lt;/p&gt;
&lt;h2&gt;为什么不用「自动翻译」省事&lt;/h2&gt;
&lt;p&gt;自动翻译可以解决一部分重复劳动，但界面文案往往很短，缺少上下文。一个词在不同页面可能有完全不同的语气。机器翻译很难保证这种一致性。&lt;/p&gt;
&lt;p&gt;==界面文案是产品语气的一部分，不能只当成字符串替换。==&lt;/p&gt;
&lt;p&gt;:::tip[新增文案的流程]
先在 &lt;code&gt;I18nKey&lt;/code&gt; 里加键，再补十个语言模块，最后在组件里引用。顺序反过来，就容易出现「组件已经上线，翻译还没跟上」的情况。
:::&lt;/p&gt;
&lt;h2&gt;中文站点的特殊处理&lt;/h2&gt;
&lt;p&gt;本站默认语言是简体中文，但 i18n 结构并没有因此简化。这样做的好处是：以后如果增加英文界面，不需要回头重构组件，只需要补翻译值。&lt;/p&gt;
&lt;p&gt;:::details[分类和标签也要翻译吗？]
分类、标签和系列名属于内容，不是界面文案。它们由作者直接填写，因此不进入 &lt;code&gt;I18nKey&lt;/code&gt; 体系。界面框架（如「分类」「标签」这些标题）才需要翻译。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;i18n 的难点不在翻译本身，而在「让所有文案都有归属」。当每个字符串都能追溯到一个键，遗漏和重复才会变得可见。&lt;/p&gt;
&lt;p&gt;对个人博客来说，这套机制可能有点重；但它带来的确定性，在长期维护中很划算。&lt;/p&gt;
</content:encoded></item><item><title>Swup 与持久化外壳：无刷新导航的代价</title><link>https://www.ycera.cn/posts/swup-navigation/</link><guid isPermaLink="true">https://www.ycera.cn/posts/swup-navigation/</guid><description>页面切换没有白屏，体验确实更好。但持久化外壳也意味着，很多逻辑不能只写一次就完事。</description><pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第一次体验 Swup 时，最直观的感受是「顺」。点击链接，页面内容淡入替换，导航栏和侧栏原地不动，没有整页白屏。&lt;/p&gt;
&lt;p&gt;但真正开始维护时，我发现无刷新导航并不是免费的。它改变的不只是体验，还有代码的生命周期。&lt;/p&gt;
&lt;h2&gt;外壳与内容的分工&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
  Shell[持久化外壳：导航、侧栏、页脚]
  Container[#swup-container：可替换页面内容]
  Shell --&amp;gt; Container
  Container --&amp;gt; PageA[页面 A]
  Container --&amp;gt; PageB[页面 B]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;外壳只渲染一次，页面内容在每次导航时被替换。这样避免了重复渲染，但也带来一个关键问题：&lt;strong&gt;外壳里的代码不会因为路由变化而重新执行&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;生命周期比事件名更重要&lt;/h2&gt;
&lt;p&gt;如果一段逻辑依赖当前页面的数据，它就不能只在首次加载时运行。常见做法是监听 Swup 的生命周期：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&quot;astro:page-load&quot;, () =&amp;gt; {
  initPageScopedFeatures();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[直接加载和客户端导航是两条路径]
首次打开页面时，走的是普通加载流程；点击站内链接时，走的是 Swup 替换流程。只在其中一条路径上初始化，就会出现「刷新后正常，点进去失效」或者相反的问题。
:::&lt;/p&gt;
&lt;p&gt;我在处理筛选器、代码块增强和图表初始化时，都同时验证了这两种路径。测试时不能只刷新页面，还要从其他页面点进来。&lt;/p&gt;
&lt;h2&gt;事件委托比重新绑定更稳&lt;/h2&gt;
&lt;p&gt;页面内容被替换后，之前绑定在旧 DOM 上的事件监听器也会一起消失。与其每次导航后重新绑定，不如把监听器放在持久化的父节点上，用事件委托处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document.addEventListener(&quot;click&quot;, (event) =&amp;gt; {
  const target = event.target as HTMLElement;
  const button = target.closest(&quot;[data-copy]&quot;);
  if (!button) return;
  copyToClipboard(button.dataset.copy ?? &quot;&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::spoiler[那为什么不干脆关掉 Swup？]
关掉当然最省事，但页面切换的连贯感也会消失。我的判断是：只要把生命周期约定写清楚，维护成本是可以接受的；真正危险的是「以为它会自动处理」。
:::&lt;/p&gt;
&lt;h2&gt;体验与复杂度的交换&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方面&lt;/th&gt;
&lt;th&gt;普通整页导航&lt;/th&gt;
&lt;th&gt;Swup 无刷新导航&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;首屏切换&lt;/td&gt;
&lt;td&gt;有白屏&lt;/td&gt;
&lt;td&gt;过渡自然&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;状态保持&lt;/td&gt;
&lt;td&gt;全部重置&lt;/td&gt;
&lt;td&gt;外壳状态保留&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码复杂度&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;需要生命周期管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调试难度&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;需要同时验证两条路径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==无刷新导航买到的是体验，付出的是对生命周期的理解。==&lt;/p&gt;
&lt;p&gt;:::tip[一个检查清单]
改动涉及页面级交互时，问自己三个问题：首次加载会执行吗？客户端导航后会执行吗？从旧页面带回的状态需要清理吗？这三个问题答完，大部分 Swup 问题就提前避开了。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Swup 不是「装上就变好」的插件。它要求开发者明确区分「一次性外壳」和「可替换页面」，并在正确的生命周期里做正确的事。&lt;/p&gt;
&lt;p&gt;理解这一点之后，无刷新导航才真正成为体验优化，而不是隐藏的 bug 来源。&lt;/p&gt;
</content:encoded></item><item><title>Markdown 扩展管线：让文章多一点表达力</title><link>https://www.ycera.cn/posts/markdown-pipeline/</link><guid isPermaLink="true">https://www.ycera.cn/posts/markdown-pipeline/</guid><description>普通 Markdown 够用，但不够有趣。本站通过 remark/rehype 管线加入提示框、步骤流、选项卡等语法，并按需加载样式。</description><pubDate>Sun, 25 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Markdown 的优势是简单，劣势也是简单。写技术文章时，我经常需要「提示」「步骤」「对照代码」这些结构，而标准语法里没有。&lt;/p&gt;
&lt;p&gt;解决方案不是换一个富文本编辑器，而是在 Markdown 解析管线上做扩展。&lt;/p&gt;
&lt;h2&gt;从文本到 HTML 的旅程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  A[Markdown 源文件] --&amp;gt; B[remark 解析与转换]
  B --&amp;gt; C[rehype 生成 HTML 节点]
  C --&amp;gt; D[自定义组件节点]
  D --&amp;gt; E[Astro 渲染输出]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;remark 负责理解 Markdown 结构，rehype 负责把结构变成 HTML。自定义语法就插在这两层之间：先识别作者写的指令，再把它转换成对应的组件节点。&lt;/p&gt;
&lt;p&gt;:::tip[扩展的前提是不破坏原文]
所有自定义语法都有一个共同约束：非法或残缺的输入必须回退成普通 Markdown，不能静默吞掉内容。这样即使解析器升级，文章也不会凭空少一段。
:::&lt;/p&gt;
&lt;h2&gt;一个容器语法长什么样&lt;/h2&gt;
&lt;p&gt;下面是本站提示框的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::tip[部署前检查]
确认构建通过，再推送生产环境。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渲染后就是带图标和强调色的提示块。它不需要在 frontmatter 里开启开关，也不需要额外配置——只要文章里出现这个语法，对应样式和运行时就会被收集。&lt;/p&gt;
&lt;h2&gt;扩展不是越多越好&lt;/h2&gt;
&lt;p&gt;每加一种语法，就多一份解析逻辑、一套样式和一组测试。真正需要控制的不是「支持多少种」，而是「哪些语法值得长期维护」。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;运行时成本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;提示框&lt;/td&gt;
&lt;td&gt;强调注意事项&lt;/td&gt;
&lt;td&gt;纯 CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;步骤流&lt;/td&gt;
&lt;td&gt;教程与流程&lt;/td&gt;
&lt;td&gt;纯 CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;选项卡&lt;/td&gt;
&lt;td&gt;对照不同方案&lt;/td&gt;
&lt;td&gt;少量客户端脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid&lt;/td&gt;
&lt;td&gt;流程图与关系图&lt;/td&gt;
&lt;td&gt;按需加载渲染器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码树&lt;/td&gt;
&lt;td&gt;展示多文件结构&lt;/td&gt;
&lt;td&gt;纯 CSS + 少量交互&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==只在实际写作中反复用到的语法，才值得进入长期清单。==&lt;/p&gt;
&lt;h2&gt;按需加载是怎么做到的&lt;/h2&gt;
&lt;p&gt;构建时会扫描文章内容，判断出现了哪些语法，再只把这些语法的样式和脚本注入当前页面。结果是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一篇纯文字文章不会带上 Mermaid 渲染器；&lt;/li&gt;
&lt;li&gt;没有选项卡的页面不会加载选项卡脚本；&lt;/li&gt;
&lt;li&gt;语法清单集中在 manifest 里，新增语法时只需要登记一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 页面只加载实际用到的语法资源
const usedSyntaxes = detectSyntaxes(articleMarkdown);
const assets = resolveAssets(usedSyntaxes, manifest);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[自定义语法也有学习成本]
语法越丰富，作者越需要记住写法。我的做法是：常用语法控制在少数几种，复杂语法只在确实需要时使用，并在文章里给出可复制的示例。
:::&lt;/p&gt;
&lt;h2&gt;写作与渲染的分工&lt;/h2&gt;
&lt;p&gt;理想状态下，作者只需要关心「我想表达什么结构」，而不是「这个结构对应哪个 HTML 标签」。提示框、步骤、选项卡这些语法，本质上都是在降低表达成本。&lt;/p&gt;
&lt;p&gt;:::details[如果语法失效了怎么办？]
管线对非法输入保留原文，所以最坏情况是读者看到原始的 &lt;code&gt;:::tip&lt;/code&gt; 文本，而不是整段内容消失。这比「渲染失败导致空白」安全得多。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Markdown 扩展管线的价值，不是把文章变成网页应用，而是让文字拥有更清晰的层次。只要守住「按需加载」和「非法回退」两条底线，扩展就可以成为写作的助力，而不是维护负担。&lt;/p&gt;
</content:encoded></item><item><title>内容集合与 Frontmatter：把写作变成数据</title><link>https://www.ycera.cn/posts/content-collections-frontmatter/</link><guid isPermaLink="true">https://www.ycera.cn/posts/content-collections-frontmatter/</guid><description>文章不是普通文件，而是有结构的数据。Astro Content Collections 让这件事在构建阶段就被检查。</description><pubDate>Sat, 24 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写博客最怕的不是没内容，而是内容写完了，页面却是空的。少一个冒号、日期格式不对、字段名拼错，都可能让文章悄悄消失。&lt;/p&gt;
&lt;p&gt;Astro 的内容集合把这类问题从「发布后才发现」提前到了「构建时直接报错」。&lt;/p&gt;
&lt;h2&gt;Frontmatter 是文章的第一层结构&lt;/h2&gt;
&lt;p&gt;::: field-group&lt;/p&gt;
&lt;p&gt;::: field title
@type string
@required&lt;/p&gt;
&lt;p&gt;文章标题。归档、搜索、RSS 和分享卡片都会用到。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: field published
@type date
@required&lt;/p&gt;
&lt;p&gt;发布日期，决定归档位置和列表排序。格式必须是 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: field draft
@type boolean
@default false
@optional&lt;/p&gt;
&lt;p&gt;草稿开关。开启后只在开发环境可见，正式构建会被排除。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: field tags
@type string[]
@default []
@optional&lt;/p&gt;
&lt;p&gt;标签数组，会进入标签聚合页。中文标签会直接作为展示文本。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::::&lt;/p&gt;
&lt;p&gt;这些字段看起来简单，但它们共同决定了一篇文章在站点里的位置、可见性和关联关系。&lt;/p&gt;
&lt;h2&gt;一个最小但完整的例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;title: 你好，新网站
published: 2026-10-31
description: 第一篇正式文章。
tags:
  - 随笔
  - 建站
category: 随笔
draft: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要 &lt;code&gt;title&lt;/code&gt; 和 &lt;code&gt;published&lt;/code&gt; 存在，文章就能被识别。其余字段都有默认值，不需要为了「填满模板」而写一堆空字段。&lt;/p&gt;
&lt;p&gt;:::important[日期不是随便写的字符串]
&lt;code&gt;published&lt;/code&gt; 必须是日历日期。如果同一天发布多篇文章，可以额外提供带时区偏移的 &lt;code&gt;publishedAt&lt;/code&gt;，例如 &lt;code&gt;2026-10-31T20:00:00+08:00&lt;/code&gt;。只写日期时，系统会用文章 ID 做稳定排序。
:::&lt;/p&gt;
&lt;h2&gt;写作流程已经固定下来&lt;/h2&gt;
&lt;p&gt;:::steps{title=&quot;从新建到发布&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;生成骨架&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;pnpm.cmd new-post my-post&lt;/code&gt;，脚手架会按站点时区写好日期。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写正文&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文件名就是 URL slug，正文直接使用 Markdown 和自定义语法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;本地预览&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;pnpm.cmd dev&lt;/code&gt;，在浏览器里确认排版、图片和链接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;正式发布&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;确认 &lt;code&gt;draft&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;，执行构建并部署 &lt;code&gt;dist/&lt;/code&gt;。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这套流程的价值在于：&lt;strong&gt;写作时只需要关心内容，结构检查交给工具&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;分类、标签和系列不是三套系统&lt;/h2&gt;
&lt;p&gt;一开始我以为分类、标签、系列要分别维护，后来发现它们其实都从文章字段长出来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tags&lt;/code&gt; 聚合出标签页；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt; 聚合出分类页；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;series&lt;/code&gt; 指向系列实体，&lt;code&gt;seriesOrder&lt;/code&gt; 决定阅读顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文章是唯一的数据源，其他页面都是它的投影。这样做的好处是不会出现「文章改了标签，标签页却没更新」的同步问题。&lt;/p&gt;
&lt;p&gt;:::tip[先写内容，再补结构]
新文章开始时，我只写标题、日期和正文。等文章真正成形，再补标签、分类和系列。过早设计分类体系，往往会写出很多用不上的空分类。
:::&lt;/p&gt;
&lt;h2&gt;一点心得&lt;/h2&gt;
&lt;p&gt;内容集合最容易被低估的地方，是它把「写作规范」变成了可以执行的代码。规范写在文档里，靠人记住；schema 写在代码里，构建时会替你检查。&lt;/p&gt;
&lt;p&gt;对于一个人维护的博客来说，后者的价值更大——因为没有人会替你 review frontmatter。&lt;/p&gt;
</content:encoded></item><item><title>Tailwind 4 与 Stylus 共存的边界</title><link>https://www.ycera.cn/posts/tailwind-and-stylus/</link><guid isPermaLink="true">https://www.ycera.cn/posts/tailwind-and-stylus/</guid><description>一个项目里同时出现 Tailwind 和 Stylus，听起来像重复建设。关键是划清它们各自负责的区域。</description><pubDate>Fri, 23 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第一次看到项目里同时使用 Tailwind 4 和 Stylus 时，我的反应是：为什么不统一成一种？&lt;/p&gt;
&lt;p&gt;用了一段时间后，我反而理解了这种组合。它们解决的不是同一类问题。&lt;/p&gt;
&lt;h2&gt;两种写法，两种职责&lt;/h2&gt;
&lt;p&gt;Tailwind 擅长的是&lt;strong&gt;页面级、组合式&lt;/strong&gt;的样式：间距、布局、对齐、响应式断点。这类样式变化频繁，写在模板里最直观。&lt;/p&gt;
&lt;p&gt;Stylus 擅长的是&lt;strong&gt;组件内部、结构化&lt;/strong&gt;的样式：伪元素、嵌套选择器、复杂的状态组合、动画关键帧。这类样式需要和组件结构绑定，放在独立的样式块里更好维护。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;区域&lt;/th&gt;
&lt;th&gt;主要工具&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;页面布局与间距&lt;/td&gt;
&lt;td&gt;Tailwind&lt;/td&gt;
&lt;td&gt;改动频繁，就地调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;组件内部结构&lt;/td&gt;
&lt;td&gt;Stylus&lt;/td&gt;
&lt;td&gt;嵌套和状态组合更清晰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;设计令牌&lt;/td&gt;
&lt;td&gt;CSS 变量&lt;/td&gt;
&lt;td&gt;跨工具共享&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;响应式断点&lt;/td&gt;
&lt;td&gt;Stylus 变量 + Tailwind&lt;/td&gt;
&lt;td&gt;统一数值来源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==工具没有优劣，边界不清才是问题。==&lt;/p&gt;
&lt;h2&gt;断点必须有单一来源&lt;/h2&gt;
&lt;p&gt;最容易出错的地方，是同一套断点在两个工具里各写一遍。项目通过一个共享的 &lt;code&gt;breakpoints.styl&lt;/code&gt; 把数值集中起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bp-sm = 640px
bp-md = 768px
bp-lg = 1024px
bp-xl = 1280px
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组件内部使用 Stylus 变量，页面布局使用 Tailwind 的响应式前缀。数值只维护一次，改的时候不会漏掉另一半。&lt;/p&gt;
&lt;p&gt;:::caution[Stylus 的 &amp;amp; 不总是拼接]
在 Stylus 里写嵌套时，&lt;code&gt;&amp;amp;__element&lt;/code&gt; 会把父选择器直接拼成新的类名，而 &lt;code&gt;&amp;amp; .element&lt;/code&gt; 才是后代选择器。这两种写法看起来很像，语义却完全不同。改样式前先确认当前文件用的是哪一种约定。
:::&lt;/p&gt;
&lt;h2&gt;一个具体例子&lt;/h2&gt;
&lt;p&gt;下面这段样式负责筛选器里的清除按钮。它需要绝对定位、圆形点击区域、悬停背景和图标尺寸，这些都属于组件内部细节，所以放在 Stylus 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;__search-clear
  position absolute
  right 0.5rem
  top 50%
  transform translateY(-50%)
  display inline-flex
  border-radius var(--shape-corner-full)

  &amp;amp;:hover
    background var(--surface-container-high)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而它在模板里的位置和外部间距，则继续交给 Tailwind：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;relative w-full max-w-lg&quot;&amp;gt;
  &amp;lt;input class=&quot;w-full&quot; /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::spoiler[那为什么不全部用 Tailwind？]
复杂组件的状态组合会写出很长的 class 字符串，可读性下降；动画关键帧和伪元素也不适合塞进模板。把内部结构交给 Stylus，模板会轻很多。
:::&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;!important&lt;/code&gt; 要有主人&lt;/h2&gt;
&lt;p&gt;样式冲突时，最省事的做法是加 &lt;code&gt;!important&lt;/code&gt;，但这也是最容易失控的做法。项目对它有明确约束：只在确实需要覆盖第三方或运行时样式的边界使用，并且要写清原因。&lt;/p&gt;
&lt;p&gt;我的经验是：&lt;strong&gt;先找层级问题，再考虑 &lt;code&gt;!important&lt;/code&gt;&lt;/strong&gt;。大多数「改不动」的样式，其实是选择器优先级或者加载顺序的问题，而不是真的需要强制覆盖。&lt;/p&gt;
&lt;p&gt;:::tip[共存的关键是约定]
Tailwind 管外、Stylus 管内，令牌管共享值。只要这条边界稳定，两个工具就不会互相打架，反而各自发挥长处。
:::&lt;/p&gt;
&lt;p&gt;现在再问我为什么不统一，我的答案会是：统一工具很轻松，统一职责才难。后者做到了，前者反而没那么重要。&lt;/p&gt;
</content:encoded></item><item><title>设计令牌与 M3E：统一不等于千篇一律</title><link>https://www.ycera.cn/posts/m3e-design-tokens/</link><guid isPermaLink="true">https://www.ycera.cn/posts/m3e-design-tokens/</guid><description>颜色、圆角、字号、动效如果每个组件各写一套，界面迟早会失控。记录本站用设计令牌收敛视觉的过程。</description><pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个网站刚开始做的时候，组件少，随手写几个颜色值并不会出问题。等到页面变多，问题就会出现：这个卡片的圆角是 12px，那个是 16px；这个标题用 500 字重，那个用 400；两个看起来差不多的灰色，其实是完全不同的值。&lt;/p&gt;
&lt;p&gt;本站使用 M3E（Material 3 Expressive）的设计令牌体系，把这类问题提前收进变量里。&lt;/p&gt;
&lt;h2&gt;令牌不是「多写一层变量」&lt;/h2&gt;
&lt;p&gt;::: field-group&lt;/p&gt;
&lt;p&gt;::: field --m3e-type-body-large
@type font shorthand
@default 500 1.0625rem/1.7
@required&lt;/p&gt;
&lt;p&gt;正文级排版。字号、行高和字重一起定义，避免组件里各写各的。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: field --shape-corner-l
@type length
@default 1rem
@optional&lt;/p&gt;
&lt;p&gt;大圆角容器使用。卡片、弹层和面板保持同一套几何语言。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: field --m3e-duration-medium
@type time
@default 300ms
@optional&lt;/p&gt;
&lt;p&gt;中等时长动效。配合统一的缓动曲线使用，避免每个组件自己调时间。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::::&lt;/p&gt;
&lt;p&gt;这些变量的意义不在于「集中管理」，而在于&lt;strong&gt;约束决策范围&lt;/strong&gt;。当我想给一个卡片加圆角时，不是问「多少像素好看」，而是问「它属于哪个圆角层级」。&lt;/p&gt;
&lt;h2&gt;排版是最容易失控的部分&lt;/h2&gt;
&lt;p&gt;中文和英文的视觉密度不同，同一套字号在两种语言里读起来完全不一样。最初本站的正文偏小，中文显得又细又密；后来我把正文调到 17px，行高提到 1.85，并把正文和标题的字重分别调到 500 和 600，阅读压力才明显下降。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;令牌&lt;/th&gt;
&lt;th&gt;调整前&lt;/th&gt;
&lt;th&gt;调整后&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;body-large&lt;/td&gt;
&lt;td&gt;16px / 400&lt;/td&gt;
&lt;td&gt;17px / 500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body-medium&lt;/td&gt;
&lt;td&gt;14px&lt;/td&gt;
&lt;td&gt;15px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;body-small&lt;/td&gt;
&lt;td&gt;12px&lt;/td&gt;
&lt;td&gt;13px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;title-medium&lt;/td&gt;
&lt;td&gt;16px / 500&lt;/td&gt;
&lt;td&gt;17px / 600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==字号不是孤立数字，它和行高、字重、字体一起决定阅读感受。==&lt;/p&gt;
&lt;p&gt;:::tip[一个实用原则]
先改令牌，再改组件。令牌是全局语言，组件只是这句话的具体说法。只改单个组件，短期看有效，长期会把设计系统撕成碎片。
:::&lt;/p&gt;
&lt;h2&gt;颜色和表面要成对出现&lt;/h2&gt;
&lt;p&gt;Material 3 的 surface / on-surface 配对，解决的是「文字放在什么背景上」的问题。只定义一个主色是不够的，还必须定义它对应的可读前景色。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.card {
  background: var(--surface-container);
  color: var(--on-surface);
}

.card--muted {
  background: var(--surface-container-low);
  color: var(--on-surface-variant);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样切换明暗主题时，不需要在每个组件里写条件判断，变量会自己完成替换。&lt;/p&gt;
&lt;p&gt;:::note[为什么仍然允许少量硬编码]
图片上的白色文字、遮罩层的黑色背景，这类与具体内容强相关的场景，固定黑白值反而更可靠。项目规则里把这类情况列为「有记录的例外」，而不是一刀切禁止。
:::&lt;/p&gt;
&lt;h2&gt;动效也要有名字&lt;/h2&gt;
&lt;p&gt;动效最容易被忽略，但它同样需要约束。本站把常用时长和缓动收进 &lt;code&gt;--m3e-duration-*&lt;/code&gt; 与 &lt;code&gt;--m3e-easing-*&lt;/code&gt;，并统一尊重 &lt;code&gt;prefers-reduced-motion&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样做的好处是：当我给一个面板加展开动画时，不需要重新思考「应该用 200ms 还是 400ms」，而是直接选择「短时长」或「中时长」。视觉节奏自然就统一了。&lt;/p&gt;
&lt;p&gt;:::important[统一的反面不是个性]
设计令牌统一的是底层语言，不是抹掉每个页面的特点。相册可以更沉浸，文章可以更安静，时间线可以更紧凑——它们只是用同一套词汇说话。
:::&lt;/p&gt;
&lt;p&gt;回头看，设计系统真正节省的不是写 CSS 的时间，而是&lt;strong&gt;反复做小决定的时间&lt;/strong&gt;。把那些决定提前做完，才能把注意力留给内容本身。&lt;/p&gt;
</content:encoded></item><item><title>Svelte 5 在博客里到底负责什么</title><link>https://www.ycera.cn/posts/svelte-5-in-blog/</link><guid isPermaLink="true">https://www.ycera.cn/posts/svelte-5-in-blog/</guid><description>博客大部分页面是静态的，但搜索、筛选、标签切换这些地方需要交互。Svelte 5 在这里的分工很明确。</description><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;静态博客也需要一点交互：搜索框、标签筛选、主题切换、折叠面板。关键在于——&lt;strong&gt;不要因为一点点交互，就让整个页面都变成客户端应用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Svelte 5 在这个项目里的角色，是「局部增强」，而不是「接管页面」。&lt;/p&gt;
&lt;h2&gt;先静态，再交互&lt;/h2&gt;
&lt;p&gt;大部分文章页、归档页、关于页都是纯 Astro 组件，构建时直接输出 HTML。只有真正需要响应点击和输入的部分，才会写成 Svelte 组件，并通过 hydration 指令挂载。&lt;/p&gt;
&lt;p&gt;::: tabs#hydration&lt;/p&gt;
&lt;p&gt;@tab 立即加载#立即&lt;/p&gt;
&lt;p&gt;适合首屏就需要立即可用的组件，例如导航栏里的搜索入口。对应的指令是 &lt;code&gt;client:load&lt;/code&gt;，代价是脚本会在页面加载后马上执行。&lt;/p&gt;
&lt;p&gt;@tab 滚动到附近再加载#可见&lt;/p&gt;
&lt;p&gt;适合首屏之外、滚动到附近才需要工作的组件。对应的指令是 &lt;code&gt;client:visible&lt;/code&gt;，文章底部的相关推荐、相册列表都可以用这个策略。&lt;/p&gt;
&lt;p&gt;@tab 仅客户端#仅客户端&lt;/p&gt;
&lt;p&gt;完全跳过服务端渲染。对应的指令是 &lt;code&gt;client:only&lt;/code&gt;，只适合依赖浏览器 API、无法在构建时渲染的组件。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;这三种策略的区别，本质上是在回答一个问题：&lt;strong&gt;这个交互最早什么时候可能被用到？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Runes 让状态更直白&lt;/h2&gt;
&lt;p&gt;Svelte 5 的 runes 写法比过去的响应式语法更接近普通 JavaScript：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script lang=&quot;ts&quot;&amp;gt;
  let query = $state(&quot;&quot;);
  let selectedTag = $state(&quot;&quot;);
  let filtered = $derived(
    items.filter((item) =&amp;gt; item.title.includes(query))
  );
&amp;lt;/script&amp;gt;

&amp;lt;input bind:value={query} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$state&lt;/code&gt; 声明可变状态，&lt;code&gt;$derived&lt;/code&gt; 声明派生值。比起手动维护依赖关系，这种写法更容易读，也更不容易漏掉更新。&lt;/p&gt;
&lt;p&gt;:::warning[不要在同一个组件里混用两种写法]
项目里有些组件仍然是旧语法。Svelte 5 允许逐步迁移，但同一个文件里混用 runes 和旧式响应式声明，会让状态来源变得难以追踪。我的做法是：改哪个组件，就把哪个组件完整迁移，而不是东拼西凑。
:::&lt;/p&gt;
&lt;h2&gt;一个真实的取舍&lt;/h2&gt;
&lt;p&gt;筛选器需要读取 URL 参数，让刷新和分享后的状态保持一致。这段逻辑只能在浏览器里运行，所以放在 &lt;code&gt;onMount&lt;/code&gt; 中；但它又不需要整个页面都变成客户端组件。&lt;/p&gt;
&lt;p&gt;==交互的边界应该由需求决定，而不是由框架决定。==&lt;/p&gt;
&lt;p&gt;我把筛选器拆成独立组件，只让它自己承担状态，页面其余部分继续静态输出。这样一来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有筛选需求的读者，不会为这段脚本付出首屏成本；&lt;/li&gt;
&lt;li&gt;需要交互的读者，点开筛选器后功能完整可用；&lt;/li&gt;
&lt;li&gt;构建时仍然能生成可读的 HTML，搜索引擎和 RSS 不受影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::details[那动画呢？]
筛选切换时的淡入和加载指示器也用 Svelte 状态驱动，但动画本身遵循系统的 &lt;code&gt;prefers-reduced-motion&lt;/code&gt; 设置。交互可以有趣，但不应该强迫所有人接受。
:::&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Svelte 5 在这个博客里不是主角，而是配角。它负责把少数需要响应的地方做得顺滑，其余部分交给 Astro 在构建时完成。这种分工听起来保守，但对内容站来说，保守往往意味着更快的首屏和更少的故障面。&lt;/p&gt;
</content:encoded></item><item><title>为什么这个博客选择 Astro</title><link>https://www.ycera.cn/posts/why-astro/</link><guid isPermaLink="true">https://www.ycera.cn/posts/why-astro/</guid><description>从零开始搭博客时，我在 SPA、服务端渲染和静态生成之间犹豫了很久，最后选择了 Astro。这篇文章讲讲原因。</description><pubDate>Tue, 20 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;搭博客之前，我先问了自己一个问题：这个站点到底需要什么？&lt;/p&gt;
&lt;p&gt;答案很朴素——写文章、放图片、偶尔加一点交互。没有用户登录，没有实时数据，也不需要复杂的后台。既然如此，把一个以内容为主的网站做成单页应用，反而是在给未来埋负担。&lt;/p&gt;
&lt;p&gt;*[SSG]: Static Site Generation，静态站点生成，在构建阶段把页面预先生成成 HTML。&lt;/p&gt;
&lt;h2&gt;我想要的三个东西&lt;/h2&gt;
&lt;p&gt;:::steps{title=&quot;选择框架时的三个硬指标&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;首屏要快&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;读者点开链接后，应该尽快看到文字，而不是先等一大包 JavaScript 下载和执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;写作要简单&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;内容应该能直接写成 Markdown，而不是先学一套页面搭建系统。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;交互要克制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只有真正需要交互的地方才加载脚本，其余部分保持纯静态。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Astro 刚好同时满足这三点。它默认输出 SSG 页面，组件在构建时渲染成 HTML；需要交互的部分再单独标记，作为「岛屿」加载。&lt;/p&gt;
&lt;h2&gt;静态生成不是退步&lt;/h2&gt;
&lt;p&gt;很多人把静态站点理解成「功能少」，但对我来说，它更像是一种取舍：把复杂度放在构建阶段，把简单留给访问者。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;首屏&lt;/th&gt;
&lt;th&gt;运维&lt;/th&gt;
&lt;th&gt;适合内容站吗&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;纯 SPA&lt;/td&gt;
&lt;td&gt;需要先加载框架&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;一般&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务端渲染&lt;/td&gt;
&lt;td&gt;依赖服务器&lt;/td&gt;
&lt;td&gt;需要持续运维&lt;/td&gt;
&lt;td&gt;可以，但偏重&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态生成&lt;/td&gt;
&lt;td&gt;直接返回 HTML&lt;/td&gt;
&lt;td&gt;可以放在任意静态托管&lt;/td&gt;
&lt;td&gt;很合适&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;==对内容站来说，静态生成不是妥协，而是把复杂度放到了正确的位置。==&lt;/p&gt;
&lt;h2&gt;真正吸引我的是内容集合&lt;/h2&gt;
&lt;p&gt;Astro 的 Content Collections 让 Markdown 不只是文件，而是有 schema 的数据。文章缺少标题、日期写错格式，构建阶段就会直接报错，而不是等发布之后才发现页面空白。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const posts = defineCollection({
  loader: glob({ pattern: &quot;**/*.{md,mdx}&quot;, base: &quot;./src/content/posts&quot; }),
  schema: z.object({
    title: z.string(),
    published: z.date(),
    tags: z.array(z.string()).optional().default([]),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段 schema 看起来普通，但它把「写作约定」变成了机器可以检查的规则。&lt;/p&gt;
&lt;p&gt;:::tip[我的体会]
选择框架时，不要只看它能不能实现功能，还要看它默认把你推向哪种工作方式。Astro 的默认值，正好是我想要的：内容优先、脚本克制、构建期校验。
:::&lt;/p&gt;
&lt;h2&gt;部署也变得简单&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm build&lt;/code&gt; 之后得到的是一个 &lt;code&gt;dist/&lt;/code&gt; 目录，里面全是 HTML、CSS、图片和少量按需加载的脚本。把它放到任意静态托管上就能访问，不需要 Node 进程常驻。&lt;/p&gt;
&lt;p&gt;:::note[代价也要说清楚]
静态站点不适合实时性很强的场景。如果以后我要做评论、搜索或者动态数据，就得额外引入服务或构建期快照。这不是 Astro 的问题，而是所有静态方案都要面对的现实。
:::&lt;/p&gt;
&lt;p&gt;现在回头看，选择 Astro 并不是因为它「最新」或者「最流行」，而是因为它和我对博客的理解一致：&lt;strong&gt;内容应该比框架更重要&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item></channel></rss>