<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>未来之蓝 | xpfxzxc 的个人博客 - 个人学习和日常零散想法的记录空间，内容随性且不定期更新</title><description>未来之蓝是 xpfxzxc 的个人博客，记录个人学习和日常零散想法。心怀蔚蓝愿景，脚踏实地前行。每一篇记录都是通向理想未来的坚实脚印。</description><link>https://xpfxzxc.github.io/</link><language>zh_CN</language><item><title>基于 Firefly 主题的 Astro 博客部署（五）：看板娘配置</title><link>https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part5/</link><guid isPermaLink="true">https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part5/</guid><description>记录了自己在基于 Firefly 主题的 Astro 博客上修改看板娘 Spine 模型配置的过程，以及在这过程中踩过的坑点，并对一些专有词汇做了简单解释。</description><pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;截至目前，我已经将博客站点配置得差不多了。我不仅将个人博客页面打扮得相当美观，还调整了站点信息，并启用了评论系统。整体上看，这些改动使我的博客有了独特风格，具备了相当的辨识度。&lt;/p&gt;
&lt;p&gt;不过，硬要说还有哪些地方有违和感的话，那就是博客页面窗口左下角处的看板娘 —— &lt;strong&gt;流萤&lt;/strong&gt;了。流萤的英文名是 &lt;strong&gt;Firefly&lt;/strong&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;改用其他看板娘：显然，选择与博客主题更适配的看板娘更好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我选择“改用其他看板娘”，如果能够找到合适模型的话。再说，我之前从未有过这方面的经验，所以有兴趣去接触一下这方面的内容。&lt;/p&gt;
&lt;p&gt;此外，目前我还没有修改过音乐播放器的配置。它的外观上与当前主题色并无明显冲突，因此我打算保持原样。&lt;/p&gt;
&lt;h2&gt;Spine 还是 Live2D？&lt;/h2&gt;
&lt;p&gt;Firefly 主题集成了 &lt;a href=&quot;http://zh.esotericsoftware.com&quot;&gt;&lt;strong&gt;Spine&lt;/strong&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://www.live2d.com/zh-CHS/&quot;&gt;&lt;strong&gt;Live2D&lt;/strong&gt;&lt;/a&gt; 的 Web 库用于渲染看板娘，分别是 &lt;a href=&quot;https://zh.esotericsoftware.com/spine-player&quot;&gt;&lt;strong&gt;Spine Web Player&lt;/strong&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://www.live2d.com/zh-CHS/sdk/about/&quot;&gt;&lt;strong&gt;Live2D Cubism SDK for Web&lt;/strong&gt;&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;在 Firefly 主题中，默认启用的 Spine 看板娘为流萤，其模型源于 &lt;strong&gt;Bilibili&lt;/strong&gt; 上某 UP 主免费分享；而默认禁用的 Live2D 看板娘则自带&lt;strong&gt;雪未来&lt;/strong&gt;和&lt;strong&gt;伊莉雅&lt;/strong&gt;两个模型。&lt;/p&gt;
&lt;p&gt;就目前主题的默认实现而言，Spine 与 Live2D 看板娘之间的功能差异在于：Live2D 支持眼睛跟随鼠标指针移动。除此之外的其他功能，如显示、播放动画、点击、拖拽，两者基本一致。但实际上，Spine 与 Live2D 之间的差异远不限于此。&lt;/p&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;Spine 是一款专业的 2D 骨骼动画软件。制作者先将角色拆分，然后将角色看成一个有多个骨骼的骨架——等同于为角色创建一套“骨骼”系统，再将拆分的部位绑定到对应的骨骼上，每个部位上附着着图片。通过旋转、移动、缩放骨骼，来带动整个角色的动作。这样，可以实现非常流畅、富有弹性的复杂动作。&lt;/p&gt;
&lt;p&gt;Live2D 是一种 2D 图像渲染技术，通过网格变形将一张静态的 2D 立绘“活化”，创造出具有立体感和生动表情的“伪 3D”效果。这点，可以在 Bilibili 直播上随便找间虚拟主播的直播间上体验 Live2D 效果，或者在一些二次元抽卡手游的角色展示页面上体验。制作者需要将角色分成多个图层（如脸、前发、后发、身体等），然后为每个部分建立网格和变形器。通过操控这些变形器，可以实现角色的转头、身体摆体、表情变化等。本质上是在 2D 平面上进行精细的扭曲和拉伸，因此它是天生为交互设计，参数系统可以轻松映射到外部输入。&lt;/p&gt;
&lt;h3&gt;优势对比&lt;/h3&gt;
&lt;p&gt;从上面分析来看，Spine 与 Live2D 所擅长的领域不一样。Spine 主要应用于 2D 游戏动画、广告和动画短片和任何需要复杂、可循环的动作的 2D 项目；而 Live2D 主要应用于虚拟主播（VTuber）、视觉小说、角色展示界面、二次元抽卡手游。&lt;/p&gt;
&lt;p&gt;由此可见，对于个人博客看板娘，Live2D 是更合适、更主流的选择，因为 Live2D 看板娘能通过小动作与用户的操作进行互动，如眨眼、微笑、挥手，或者眼瞳追踪着鼠标指针移动，而不需要做出跑、跳等各种复杂动作。&lt;/p&gt;
&lt;p&gt;顺便一提，GitHub 上有个比较知名的 Live2D 看板娘项目：&lt;a href=&quot;https://github.com/stevenjoezhang/live2d-widget&quot;&gt;&lt;strong&gt;Live2D Widget&lt;/strong&gt;&lt;/a&gt;，可以用来快速、更好地在网页中添加 Live2D 看板娘。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;stevenjoezhang/live2d-widget&quot;}&lt;/p&gt;
&lt;p&gt;还有人专门收集了一些 Live2D 模型：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Eikanya/Live2d-model&quot;}&lt;/p&gt;
&lt;h3&gt;最终选择&lt;/h3&gt;
&lt;p&gt;针对我之前选择的个人头像和背景壁纸，我使用搜索引擎或者在一些网站上查找相关合适的 Spine 或 Live2D 模型。由于我之前未曾涉足过 2D 角色动画领域，所以经过一番搜寻，最终只找到了两个有用的网站：Bilibili 和&lt;a href=&quot;https://www.aplaybox.com&quot;&gt;&lt;strong&gt;模之屋&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在反复考虑后，我决定选用 Bilibili 上 &lt;code&gt;SunaookamiRukari&lt;/code&gt; UP 主免费分享的&lt;a href=&quot;https://www.bilibili.com/video/BV1ELtFznEMQ&quot;&gt;&lt;strong&gt;砂狼白子 Spine 模型&lt;/strong&gt;&lt;/a&gt; 作为看板娘（感谢 UP 主分享！）。该资源的目录结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shiroko
├── NP0172_spr.atlas
├── NP0172_spr.png
├── NP0172_spr.skel
└── NP0172_spr-avatar.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这款模型挺好看的，但老实说，它过于简单、静态——一个角色静止站立着，头顶上的光环微微上下浮动，眼睛和口型有好几种动画但是是静止的，除此之外其他部位都没有变动。&lt;/p&gt;
&lt;p&gt;作为一个非专业的模型使用者，我只能在网上有什么就用什么。选择 Spine 还是 Live2D 模型作为看板娘，主要取决于能找到什么，以及哪个模型的质量更好。虽然我更想用 Live2D，但限于个人能力，耗费大量时间也未能找到合适的，最终只好先用着 Spine 模型。有胜于无，仅此而已。&lt;/p&gt;
&lt;h2&gt;需求&lt;/h2&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;支持在网页窗口内自由拖拽看板娘&lt;/li&gt;
&lt;li&gt;看板娘头上的光环始终保持上下浮动&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Spine 编辑器&lt;/h2&gt;
&lt;p&gt;在后面的实操部分，会使用 Spine 编辑器对从网上下载的看板娘进行微调，并重新导出模型文件。&lt;/p&gt;
&lt;p&gt;相关文档链接：&lt;a href=&quot;https://zh.esotericsoftware.com/spine-editor-documentation&quot;&gt;Spine 编辑器文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;版本&lt;/h3&gt;
&lt;p&gt;目前 Spine 编辑器的最新版本是 4.3.x，在此版本下，官网提供了三种不同的发行版供用户选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;试用版：免费试用。总是运行最新版本的 Spine，包含了专业版的所有功能，但不支持保存项目和导出资源功能&lt;/li&gt;
&lt;li&gt;基础版：需付费。仅支持基础功能，不包含高级功能，可以运行任何较旧的 Spine 版本&lt;/li&gt;
&lt;li&gt;专业版：需付费。支持所有功能，可以运行任何较旧的 Spine 版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于编辑器各发行版支持的具体功能与区别，详见官方购买页面：&lt;a href=&quot;https://zh.esotericsoftware.com/spine-purchase&quot;&gt;购买 Spine&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;鉴于目前网上可获得的破解版仅为 &lt;a href=&quot;https://github.com/scnon/spine&quot;&gt;3.8.75 专业版&lt;/a&gt;。本次实操将基于此版本进行。&lt;/p&gt;
&lt;h3&gt;用户界面&lt;/h3&gt;
&lt;p&gt;默认情况下，Spine 界面的左侧是&lt;strong&gt;视口&lt;/strong&gt;，右侧是&lt;strong&gt;层级树视图&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;视口是显示骨骼、设置骨骼及制作骨骼动画的主要区域。底部的主工具栏可访问各种&lt;a href=&quot;https://zh.esotericsoftware.com/spine-tools&quot;&gt;&lt;strong&gt;工具和设置&lt;/strong&gt;&lt;/a&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;/ul&gt;
&lt;p&gt;在动画模式下，默认 Spine 的底部有&lt;a href=&quot;https://zh.esotericsoftware.com/spine-dopesheet&quot;&gt;&lt;strong&gt;摄影表&lt;/strong&gt;&lt;/a&gt;视图，用于显示和编辑动画的关键帧时间点。&lt;/p&gt;
&lt;p&gt;另外，还有一种视图比较有用：&lt;a href=&quot;https://zh.esotericsoftware.com/spine-graph&quot;&gt;&lt;strong&gt;曲线图&lt;/strong&gt;&lt;/a&gt;视图。这可在 Spine 窗口右上角的 &lt;code&gt;视图&lt;/code&gt; 选择框访问该视图。曲线图可显示和编辑动画的关键帧的时间点和值。关键帧值绘制在 Y 轴上，时间绘制在 X 轴上。由此产生的曲线是一条显示了该值如何随时间变化的线。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
在 4.x.x 版本下，默认 Spine 的底部有曲线图视图，旁边有一个摄影表视图标签。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-ui-setup-mode.webp&quot; alt=&quot;Spine 用户界面 - 设置模式&quot; /&gt;
&lt;img src=&quot;spine-editor-ui-animation-mode.webp&quot; alt=&quot;Spine 用户界面 - 动画模式&quot; /&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;移动/旋转模式：点击鼠标右键可切换模式。启用后，在视口中按住鼠标左键并拖动，即可控制选中骨骼或图片的移动或旋转&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;层级树视图&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-tree&quot;&gt;层级树视图&lt;/a&gt;为骨架及其包含的所有内容提供了一个层次结构视图:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-skeletons&quot;&gt;&lt;strong&gt;骨架&lt;/strong&gt;&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;表示可设置动画的角色或对象&lt;/li&gt;
&lt;li&gt;包含骨骼、插槽、附件和其他部分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-bones&quot;&gt;&lt;strong&gt;骨骼&lt;/strong&gt;&lt;/a&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;li&gt;骨骼与骨骼之间可以有父子关系，骨骼的变换会影响其子骨骼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-slots&quot;&gt;&lt;strong&gt;插槽&lt;/strong&gt;&lt;/a&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;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-attachments&quot;&gt;&lt;strong&gt;附件&lt;/strong&gt;&lt;/a&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;/li&gt;
&lt;li&gt;其他部分
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-constraints&quot;&gt;&lt;strong&gt;约束&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-slots#%E7%BB%98%E5%88%B6%E9%A1%BA%E5%BA%8F&quot;&gt;&lt;strong&gt;绘制顺序&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-skins&quot;&gt;&lt;strong&gt;皮肤&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-events&quot;&gt;&lt;strong&gt;事件&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-animations-view&quot;&gt;&lt;strong&gt;动画&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某些层级树节点在层级树的左侧边缘可能有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可见点：单击可见点后将显示或隐藏该项目&lt;/li&gt;
&lt;li&gt;钥匙按钮：在动画模式下，单击该钥匙将为该项目设置一个关键帧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在层级树种选择某个项后，其属性将显示在层级树的底部。这些属性是项目中的项的配置位置。某些属性只能在选择单个节点时进行编辑。大多数属性的右上角都有三个相同的按钮，分别用于复制、重命名和删除。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-hierarchy-tree-view.webp&quot; alt=&quot;Spine 层级树视图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;纹理解包器&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;纹理图集&lt;/strong&gt;由一个文件扩展名为“.atlas”的图集文件和一个或多个称为图集“页面图片”的图片文件组成。图集文件描述每个打包的较小图片在页面图片中的位置，称为图集“区域”。这些区域在图集文件中按名称引用。纹理图集可在项目导出数据时打包后可以得到。&lt;/p&gt;
&lt;p&gt;一个纹理图集可以包含多个页面图片，从而可将应用程序的所有图片打包到单个图集中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;纹理解包器&lt;/strong&gt;可读取纹理图集，并从中根据图集文件将一个或多个页面图片分解成多个较小的图片。&lt;/p&gt;
&lt;p&gt;打开操作路径：&lt;code&gt;Spine 标志&lt;/code&gt;(左上角) → &lt;code&gt;主菜单&lt;/code&gt; → &lt;code&gt;纹理解包器&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-texture-unpacker.webp&quot; alt=&quot;纹理解包器&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Spine Web Player&lt;/h2&gt;
&lt;p&gt;要在 Firefly 主题中通过 Spine Web Player 渲染模型。需完成以下工作：准备好模型的资源文件，选择适配的 Spine Web Player 版本，且在必要时使用 Spine 编辑器重新导出资源文件。&lt;/p&gt;
&lt;p&gt;相关文档链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-player&quot;&gt;Spine Web Player&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-runtimes-guide&quot;&gt;Spine 运行时指南&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;资源文件&lt;/h3&gt;
&lt;p&gt;从网上下载下来的 Spine 模型资源文件，必须包含三个文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;骨骼数据文件
&lt;ul&gt;
&lt;li&gt;包含了骨骼结构、动画、约束、皮肤等信息，但不包含图片&lt;/li&gt;
&lt;li&gt;格式：&lt;code&gt;.json&lt;/code&gt; 或 &lt;code&gt;.skel&lt;/code&gt;（二进制格式）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图集文件
&lt;ul&gt;
&lt;li&gt;记录了角色所有拆分散图是如何从一张（或多张）大图中拼接出来的&lt;/li&gt;
&lt;li&gt;格式：&lt;code&gt;.atlas&lt;/code&gt;，一个纯文本文件，里面定义了：
&lt;ul&gt;
&lt;li&gt;使用了哪张（些）大图（&lt;code&gt;.png&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;每个小部件（如“左手”、“头部”、“武器”）在大图中的精确位置（&lt;code&gt;x&lt;/code&gt;，&lt;code&gt;y&lt;/code&gt;，&lt;code&gt;width&lt;/code&gt;，&lt;code&gt;height&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;以及其他渲染信息（如旋转、偏移等）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;纹理文件
&lt;ul&gt;
&lt;li&gt;一张或多张“图集大图”，由 Spine 编辑器自动将拆分好的所有小部件打包生成&lt;/li&gt;
&lt;li&gt;格式：&lt;code&gt;.png&lt;/code&gt;（最常用）&lt;/li&gt;
&lt;li&gt;例如：一个角色可能只有一张 &lt;code&gt;role1.png&lt;/code&gt;，它里面就包含了这个角色的所有身体部件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;版本&lt;/h3&gt;
&lt;p&gt;Spine Web Player 的版本号（主版本号.次版本号）必须与用来导出 skeleton 和 atlas 的 Spine 编辑器版本相匹配。用来导出的 Spine 编辑器版本会被记录在导出来的骨骼数据文件里，不管是 JSON 格式还是二进制格式，都可以借助 &lt;strong&gt;VSCode&lt;/strong&gt; 打开后轻易地看出来。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;skeleton&quot;:{&quot;hash&quot;:&quot;lzvNuUiXxTkT73S+7cJC27oo7Q0&quot;,&quot;spine&quot;:&quot;3.8.75&quot;,&quot;x&quot;:-290.8,...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面是某个模型的 JSON 格式的骨骼数据文件内容，其中，&lt;code&gt;spine&lt;/code&gt; 字段的值就是用来导出的 Spine 编辑器版本，这表明应该用 3.8 版本的 Spine Web Player 去加载该模型。&lt;/p&gt;
&lt;h3&gt;重导出&lt;/h3&gt;
&lt;p&gt;有时从网上下载了模型资源文件，预览后发现有些不太满意的地方，或者想调整下导出资源时的关键设置，如图片资源更新，动画调整，输出格式调整，图集设置调整等。调整后，重新导出令人满意的“核心三件套”：骨骼数据文件、图集文件和纹理大图。&lt;/p&gt;
&lt;p&gt;如果下载的资源还附带了 Spine 项目文件 &lt;code&gt;.spine&lt;/code&gt;，那重导出的过程就很简单。但要是没有附带了 Spine 项目文件 &lt;code&gt;.spine&lt;/code&gt;，那可能没那么容易调整了。幸运的是，仅凭骨骼数据文件、图集文件和纹理大图，在大多数情况下是可以还原出可用的 Spine 项目文件的。可以还原的部分有：&lt;/p&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;有些部分内容没法还原。不管怎样，能够还原出 Spine 项目文件的完整程度主要取决于之前导出“核心三件套”的完整程度。&lt;/p&gt;
&lt;h3&gt;API 中关键概念&lt;/h3&gt;
&lt;h4&gt;RGBA8888&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;RGBA8888&lt;/strong&gt; 是一种像素颜色存储格式，用红（R）、绿（G）、蓝（B）、透明度（A）四个通道存储颜色，“8888”则表示每个通道占用 8 比特。值得注意的是，A 实际代表“不透明度”而不是“透明度”，也就是说 A=0 是完全透明，A=255 是完全不透明。例如：&lt;code&gt;0xFF0000FF&lt;/code&gt; 标识不透明的红色（即 R=255, G=0, B=0, A=255）。&lt;/p&gt;
&lt;h4&gt;预乘 Alpha&lt;/h4&gt;
&lt;p&gt;正如字面意思，&lt;strong&gt;预乘 Alpha&lt;/strong&gt;（&lt;strong&gt;Premultiplied Alpha&lt;/strong&gt;）就是让 RGB 通道预先与 A 通道相乘，A 通道仍独立存储。官方建议对所有 Spine Web Player 需显示的资产均使用预乘 Alpha，它减少了不使用预乘 Alpha 时可能出现的伪影和接缝问题。&lt;/p&gt;
&lt;h4&gt;视口&lt;/h4&gt;
&lt;p&gt;在 Spine Web Player 中，&lt;strong&gt;视口&lt;/strong&gt;是指骨架所在的世界坐标空间中的一个矩形区域，再加上填充量。&lt;/p&gt;
&lt;p&gt;默认情况下，播放器会根据动画边界框并在四周加上默认 10% 的填充自动生成一个视口。在初始化播放器的时候，可以传入 &lt;code&gt;viewport&lt;/code&gt; 字段来配置全局视口或动画专属视口。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;viewport&lt;/code&gt; 字段接受以下子字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt;: 指定视口左下角在骨架的世界坐标空间中的位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;width&lt;/code&gt;、&lt;code&gt;height&lt;/code&gt;：指定视口的尺寸&lt;/li&gt;
&lt;li&gt;&lt;code&gt;padLeft&lt;/code&gt;、&lt;code&gt;padRight&lt;/code&gt;、&lt;code&gt;padTop&lt;/code&gt;、&lt;code&gt;padBottom&lt;/code&gt;：指定要添加到视口各边的填充量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;animations&lt;/code&gt;：为特定动画指定专属的视口设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Spine 的二维视图中，X 轴是水平轴（从左到右），Y 轴是垂直轴（从下到上）。&lt;/p&gt;
&lt;p&gt;不管使用什么样的视图，播放器会保持视口长宽比，然后将视口嵌入到播放器的可用空间中。这过程中可能会保持长宽比缩放视口的显示内容，最终显示在播放器的内容可能溢出视口范围。为了方便调试，可以通过指定 &lt;code&gt;debugRender&lt;/code&gt; 字段来可视化视口。&lt;/p&gt;
&lt;h4&gt;轨道&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;轨道&lt;/strong&gt;（&lt;strong&gt;Track&lt;/strong&gt;）用于分层应用动画功能。每个轨道都存储了一个动画和其播放参数，并通过一个从零开始的编号进行索引，该编号在运行时对应其内部数组下标。系统会按照轨道编号从小到大的顺序，依次应用各个轨道上的动画。&lt;/p&gt;
&lt;p&gt;多轨道设计使得角色的各个部位可以独立播放动画。通过分别控制各轨道上的动画，能够复用动画资源，并动态组合出复杂的动作。例如，要创建“移动中射击”的动画，只需在轨道 0 上设置脚部移动动画，在轨道 1 上设置手臂射击动画，两者同时播放即可实现。&lt;/p&gt;
&lt;h3&gt;API 中的一些类&lt;/h3&gt;
&lt;p&gt;尽管 Spine Web Player 在运行时运用到了很多类，但对于我的需求来说，只有几个类需要稍微了解一下。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-api-reference#Animation&quot;&gt;&lt;strong&gt;Animation&lt;/strong&gt;&lt;/a&gt; 就是动画实例，存储着动画的一个时间轴的列表。&lt;a href=&quot;https://zh.esotericsoftware.com/spine-api-reference#AnimationState&quot;&gt;&lt;strong&gt;AnimationState&lt;/strong&gt;&lt;/a&gt; 相当于动画的“总导演”，控制和管理着动画的播放。&lt;a href=&quot;https://zh.esotericsoftware.com/spine-api-reference#TrackEntry&quot;&gt;&lt;strong&gt;TrackEntry&lt;/strong&gt;&lt;/a&gt; 是轨道条目，存储着动画播放的设置和其他状态。&lt;/p&gt;
&lt;p&gt;AnimationState 管理着多个轨道插槽。每个轨道插槽都是一个双向队列，管理着多个 TrackEntry，指明了动画播放顺序。每个 TrackEntry 存储着应用于该轨道条目的 Animation。&lt;/p&gt;
&lt;p&gt;就我的需求而言，除了初始化播放器的必要调用外，仅需使用 AnimationState 的 &lt;a href=&quot;https://zh.esotericsoftware.com/spine-api-reference#AnimationState-setAnimation&quot;&gt;&lt;code&gt;setAnimation (	int trackIndex, string animationName, bool loop): TrackEntry&lt;/code&gt;&lt;/a&gt; 方法就足够了。&lt;/p&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;由于直接修改 Firefly 主题源码涉及代码段较长且分散，不利于逐步讲解，因此我将首先实现一个简单的 Demo。此外，在前期用较短代码实现核心功能，即使出了问题也容易定位和调试，之后再将验证过的方案集成到主题中。&lt;/p&gt;
&lt;p&gt;根据之前列出的看板娘需求，以及目前主题源码中看板娘的实现，Demo 要实现的功能有：&lt;/p&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;剩余的需求无需在 Demo 中实现，因为它们要么已被主题直接支持，要么只需简单配置即可满足。&lt;/p&gt;
&lt;h3&gt;1. 查看模型版本&lt;/h3&gt;
&lt;p&gt;在引入 Spine Web Player 到页面前，需要先确认导出模型文件的 Spine 编辑器版本。这是确保运行时兼容的关键步骤。&lt;/p&gt;
&lt;p&gt;用 VSCode 打开先前下载的模型资源文件夹中的 &lt;code&gt;NP0172_spr.skel&lt;/code&gt; 文件，在弹出的警告窗口中点击 &lt;code&gt;Open Anyway&lt;/code&gt; 按钮，并在顶部选择 &lt;code&gt;Text Editor&lt;/code&gt; 项。打开后，在第一行中可看到 &lt;code&gt;3.8.96&lt;/code&gt; 字样。由此可知，该看板娘是由 Spine 编辑器 3.8.96 版本导出的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./np0172-spr-skel-first-line.webp&quot; alt=&quot;用 VSCode 打开 NP0172_spr.skel 文件后显示的第一行内容&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 引入 Spine Web Player&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.esotericsoftware.com/spine-versioning&quot;&gt;Spine 运行时版本号使用 &lt;code&gt;majar.minor&lt;/code&gt; 格式&lt;/a&gt;。在搜索引擎和 CDN 网站上难以搜到 Spine Web Player 3.8 的 JavaScript 和 CSS 资源链接。不过，最终还是在 &lt;a href=&quot;https://github.com/EsotericSoftware/spine-runtimes/tree/3.8&quot;&gt;Spine 运行时 GitHub 存储库&lt;/a&gt;中找到了 3.8 版本构建后的 &lt;a href=&quot;https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/build/spine-player.js&quot;&gt;&lt;code&gt;build/spine-player.js&lt;/code&gt;&lt;/a&gt; 文件、构建前的 &lt;a href=&quot;https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/player/src/Player.ts&quot;&gt;&lt;code&gt;player/src/Player.ts&lt;/code&gt;&lt;/a&gt; 文件和 &lt;a href=&quot;https://github.com/EsotericSoftware/spine-runtimes/blob/3.8/spine-ts/player/css/spine-player.css&quot;&gt;&lt;code&gt;player/css/spine-player.css&lt;/code&gt;&lt;/a&gt; 文件。之后，引入的时候要用 &lt;code&gt;spine-player.js&lt;/code&gt; 文件，而在参考 API 的时候要查阅 &lt;code&gt;Player.ts&lt;/code&gt; 文件，毕竟官网 Spine 运行时 API 文档是基于最新版本 4.3.x 的。&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;spine-web-player-demo&lt;/code&gt; 目录，接下来在该目录中创建以下文件与子目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spine-web-player-demo
├── server.js
└── public
    ├── index.html
    ├── NP0172_spr.atlas
    ├── NP0172_spr.png
    ├── NP0172_spr.skel
    ├── spine-player.css
    └── spine-player.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出于安全考虑，大多数现代浏览器在 &lt;code&gt;file://&lt;/code&gt; 上下文中不允许通过 &lt;code&gt;fetch&lt;/code&gt; 或 &lt;code&gt;XMLHttpRequest&lt;/code&gt; 加载本地文件，因此需要搭建一个本地服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const http = require(&apos;http&apos;);
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);

const PORT = 3000;
const ROOT_DIR = &apos;./public&apos;;

// MIME 类型映射
const MIME = {
  &apos;.js&apos;: &apos;text/javascript&apos;,
  &apos;.css&apos;: &apos;text/css&apos;,
  &apos;.json&apos;: &apos;application/json&apos;,
  &apos;.png&apos;: &apos;image/png&apos;,
  &apos;.skel&apos;: &apos;application/octet-stream&apos;,
  &apos;.atlas&apos;: &apos;text/plain; charset=utf-8&apos;,
  &apos;.html&apos;: &apos;text/html&apos;
};

const server = http.createServer((req, res) =&amp;gt; {
  // 默认首页
  let filePath = path.join(ROOT_DIR, req.url === &apos;/&apos; ? &apos;index.html&apos; : req.url);

  // 获取扩展名
  const ext = path.extname(filePath).toLowerCase();
  const contentType = MIME[ext] || &apos;application/octet-stream&apos;;

  // 判断是否用文本模式（utf8）读取
  const isText = [&apos;.js&apos;, &apos;.css&apos;, &apos;.json&apos;, &apos;.atlas&apos;, &apos;.html&apos;].includes(ext);

  fs.readFile(filePath, isText ? &apos;utf8&apos; : null, (err, data) =&amp;gt; {
    if (err) {
      res.writeHead(404);
      res.end(&apos;404&apos;);
    } else {
      res.writeHead(200, { &apos;Content-Type&apos;: contentType });
      res.end(data);
    }
  });
});

server.listen(PORT, () =&amp;gt; {
  console.log(`Local test server running at http://localhost:${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 Spine Web Player 的 JavaScript 和 CSS 文件引入到页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
  &amp;lt;title&amp;gt;Spine Web Player Demo&amp;lt;/title&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;spine-player.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;script src=&quot;spine-player.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 显示看板娘&lt;/h3&gt;
&lt;p&gt;参考 Spine Web Player 文档和 &lt;code&gt;Player.ts&lt;/code&gt; 文件源码，将播放器嵌入到页面上。在 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 标签里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&quot;player-container&quot; style=&quot;width: 640px; height: 480px;&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script src=&quot;spine-player.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
  new spine.SpinePlayer(&quot;player-container&quot;, {
    skelUrl: &quot;NP0172_spr.skel&quot;,
    atlasUrl: &quot;NP0172_spr.atlas&quot;,
    showControls: true,
    // 背景透明
    alpha: true,
    backgroundColor: &quot;#00000000&quot;,
    premultipliedAlpha: true,
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;showControls&lt;/code&gt; 配置属性来显示播放器的控件，这样可以手动切换动画。&lt;/p&gt;
&lt;p&gt;在浏览器中访问 &lt;code&gt;http://localhost:3000&lt;/code&gt;，观察到看板娘的脸部显示存在异常，出现了较大面积的空白区域：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;kanban-premultiplied-alpha-true.webp&quot; alt=&quot;看板娘的显示，premultipliedAlpha 字段的值为 true&quot; /&gt;&lt;/p&gt;
&lt;p&gt;尝试将 &lt;code&gt;premultipliedAlpha&lt;/code&gt; 字段的值设置成 &lt;code&gt;false&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;kanban-premultiplied-alpha-false.webp&quot; alt=&quot;看板娘的显示，premultipliedAlpha 字段的值为 false&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结果看板娘的脸部显示仍然存在异常：前部头发偏白色。这可能是因为作者用 Spine 编辑器导出资源时，没启用“预乘 Alpha”。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;strong&gt;ImageMagick&lt;/strong&gt; 工具快速解决该问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick public/NP0172_spr.png -channel RGB -fx &quot;u*a&quot; public/NP0172_spr.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对图集的 RGB 通道，将像素值(u)乘以透明度(a)，A 通道保持不变。这完全符合 Spine 的预乘定义。&lt;/p&gt;
&lt;p&gt;仍然将 &lt;code&gt;premultipliedAlpha&lt;/code&gt; 字段的值为 &lt;code&gt;true&lt;/code&gt;，再次测试，此时看板娘的显示正常。&lt;/p&gt;
&lt;h3&gt;4. 还原 Spine 项目&lt;/h3&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;code&gt;01&lt;/code&gt; 动画）或者浮动幅度过小（如 &lt;code&gt;Idle_01&lt;/code&gt; 动画）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于轻微上下位移的问题，是由于初始化播放器时未通过 &lt;code&gt;viewport&lt;/code&gt; 字段手动指定视口位置和尺寸，播放器此时会自动计算并设置视口。若需手动指定 &lt;code&gt;viewport&lt;/code&gt; 字段，则需要提前获取骨架所在世界坐标空间中的位置和尺寸。&lt;/p&gt;
&lt;p&gt;使用 Spine 编辑器就能很好地解决这两个问题，所以接下来要先还原出 Spine 项目。&lt;/p&gt;
&lt;p&gt;打开 Spine 编辑器后，点击 &lt;code&gt;Spine 标志&lt;/code&gt;(左上角) → &lt;code&gt;主菜单&lt;/code&gt; → &lt;code&gt;导入数据&lt;/code&gt; 导入 &lt;code&gt;NP0172_spr.skel&lt;/code&gt; 骨骼数据：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-import-kanban-skeleton-data.webp&quot; alt=&quot;Spine 编辑器上导入看板娘骨骼数据&quot; /&gt;&lt;/p&gt;
&lt;p&gt;导入成功后，会提示贴图缺失：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-missing-textures-after-import.webp&quot; alt=&quot;Spine 编辑器上导入看板娘骨骼数据后提示贴图缺失&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以接下来要导入贴图，点击 &lt;code&gt;Spine 标志&lt;/code&gt;(左上角) → &lt;code&gt;主菜单&lt;/code&gt; → &lt;code&gt;纹理解包器&lt;/code&gt;，&lt;code&gt;图集文件&lt;/code&gt;选择 &lt;code&gt;NP0172_spr.atlas&lt;/code&gt;，&lt;code&gt;输出文件夹&lt;/code&gt;路径随意，不勾选 &lt;code&gt;非预乘 alpha&lt;/code&gt; 选项：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-texture-unpacker-kanban-texture.webp&quot; alt=&quot;Spine 编辑器上使用纹理解包器解包看板娘图集&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;解开&lt;/code&gt; 按钮。解包成功后，在输出文件夹里会发现纹理文件被拆成多张贴图。&lt;/p&gt;
&lt;p&gt;在右侧层级树视图中选择 &lt;code&gt;动画&lt;/code&gt; 节点，其属性将显示在层级树视图的底部，然后选择刚才的输出文件夹路径作为&lt;code&gt;图片文件&lt;/code&gt;的&lt;code&gt;路径&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-hierarchy-image-paths.webp&quot; alt=&quot;Spine 编辑器上图集文件路径&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样，就成功通过模型资源文件还原得到 Spine 项目文件了。&lt;/p&gt;
&lt;h3&gt;5. 分离并修改光环动画&lt;/h3&gt;
&lt;p&gt;为了使看板娘头顶上的光环浮动效果更明显，可以加大光环上下浮动的距离。同时，为了避免动画切换时，光环出现“瞬移”现象，有必要将光环浮动动画独立出来。这样，点击后看板娘的眼睛和口型发生变化时，光环浮动动画不会受到影响。&lt;/p&gt;
&lt;p&gt;在 Spine 编辑器中，点击视口左上角的图标切换到动画模式，然后展开层级树视图中&lt;code&gt;动画&lt;/code&gt;节点，接着依次展开 &lt;code&gt;root&lt;/code&gt; &amp;gt; &lt;code&gt;PC_Layer&lt;/code&gt; &amp;gt; &lt;code&gt;halo&lt;/code&gt; 节点。&lt;/p&gt;
&lt;p&gt;可以通过点击动画节点的子节点左边的可见点来显示或隐藏对应的动画，但每次只能显示一个。&lt;/p&gt;
&lt;p&gt;显示动画后，选中 &lt;code&gt;root&lt;/code&gt; 节点或其子节点，就能在左侧视口底部的摄影表视图中看见该节点的关键帧变化，同时视图底部的主工具栏会显示旋转、移动、缩放、倾斜等参数。按住 &lt;code&gt;Ctrl&lt;/code&gt; 键可以进行多选。按 &lt;code&gt;Esc&lt;/code&gt; 键可取消选中这些节点，此时摄影表视图中会显示当前动画所涉及到的节点及其所有关键帧。&lt;/p&gt;
&lt;p&gt;拖动摄影表视图里的时间轴或者点击 &lt;code&gt;▶&lt;/code&gt; 按钮，可以播放当前显示中的动画。&lt;/p&gt;
&lt;p&gt;经过反复操作可以发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;光环在 &lt;code&gt;halo&lt;/code&gt; 插槽中&lt;/li&gt;
&lt;li&gt;仅 &lt;code&gt;Idle_01&lt;/code&gt; 动画里有光环的浮动效果，&lt;code&gt;PC_Layer&lt;/code&gt; 骨骼有轻微移动&lt;/li&gt;
&lt;li&gt;其他动画里光环没有浮动效果，&lt;code&gt;PC_Layer&lt;/code&gt; 骨骼没有移动，但有眼睛和口型的变化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了实现动画分层，需要创建一个单独用于光环的 &lt;code&gt;halo_float&lt;/code&gt; 动画：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在层级树视图中选中 &lt;code&gt;Idle_01&lt;/code&gt; 动画节点，点击底部属性右下第一个按钮来复制该节点&lt;/li&gt;
&lt;li&gt;将新的动画节点重命名为 &lt;code&gt;halo_float&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;紧接着将光环上下浮动效果调整得更明显：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选中 &lt;code&gt;halo&lt;/code&gt; 骨骼节点，在摄影表视图里选中第 50 帧，在主工具栏中，将&lt;code&gt;移动&lt;/code&gt;字段的 &lt;code&gt;1251.8&lt;/code&gt; 改成 &lt;code&gt;1274.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;点击旁边的红色钥匙按钮，设置一个移动的关键帧&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-set-move-keyframe-main-toolbar.webp&quot; alt=&quot;Spine 编辑器上在主工具栏中设置一个移动的关键帧&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后将多余的动画效果删除，注意不要遗漏第 0 帧的关键帧：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选中 &lt;code&gt;PC_Layer&lt;/code&gt; 骨骼节点，在摄影表视图里将 &lt;code&gt;PC_Layer&lt;/code&gt; &lt;a href=&quot;https://zh.esotericsoftware.com/spine-dopesheet#%E9%AA%A8%E9%AA%BC%E8%A1%8C&quot;&gt;&lt;strong&gt;骨骼行&lt;/strong&gt;&lt;/a&gt;里的 &lt;code&gt;移动&lt;/code&gt; &lt;a href=&quot;https://zh.esotericsoftware.com/spine-dopesheet#%E5%B1%9E%E6%80%A7%E8%A1%8C&quot;&gt;&lt;strong&gt;属性行&lt;/strong&gt;&lt;/a&gt;中的关键帧全部删除&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;Idle_01&lt;/code&gt; 动画节点左边的可见点，然后选中 &lt;code&gt;halo&lt;/code&gt; 骨骼节点，在摄影表视图中将 &lt;code&gt;halo&lt;/code&gt; 骨骼行的所有属性行中的关键帧全部删除&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6. 导出资源&lt;/h3&gt;
&lt;p&gt;点击 &lt;code&gt;Spine 标志&lt;/code&gt;(左上角) → &lt;code&gt;主菜单&lt;/code&gt; → &lt;code&gt;导出...&lt;/code&gt;，&lt;code&gt;数据&lt;/code&gt; 字段选择 &lt;code&gt;JSON&lt;/code&gt;，输出文件夹路径随意，&lt;code&gt;纹理图集&lt;/code&gt; 字段勾选 &lt;code&gt;打包&lt;/code&gt; → 选择 &lt;code&gt;附件&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-export-assets-window.webp&quot; alt=&quot;Spine 编辑器导出资源窗口&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后点击 &lt;code&gt;打包设置&lt;/code&gt; 按钮弹出 &lt;code&gt;纹理打包器设置&lt;/code&gt; 窗口，勾选 &lt;code&gt;输出&lt;/code&gt; 部分里的 &lt;code&gt;预乘 Alpha&lt;/code&gt; 选项：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spine-editor-texture-packing-settings-window.webp&quot; alt=&quot;Spine 编辑器打包设置窗口&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;确定&lt;/code&gt; 按钮返回到上一个窗口，接着点击 &lt;code&gt;导出&lt;/code&gt; 按钮导出资源。&lt;/p&gt;
&lt;p&gt;导出资源完毕后，确保 &lt;code&gt;public&lt;/code&gt; 目录下至少要包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public
├── index.html
├── NP0172_spr.atlas
├── NP0172_spr.json
├── NP0172_spr.png
├── spine-player.css
└── spine-player.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. 修改 Spine 版本号&lt;/h3&gt;
&lt;p&gt;由于当前已改用 JSON 格式的骨骼数据文件，因此代码中在指定骨骼数据文件路径时，应当使用 &lt;code&gt;jsonUrl&lt;/code&gt; 字段替代原先的 &lt;code&gt;skelUrl&lt;/code&gt; 字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-skelUrl: &quot;NP0172_spr.skel&quot;,
+jsonUrl: &quot;NP0172_spr.json&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时尝试运行会发现，Spine 播放器提示错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error: could not load skeleton .json.
Error: Unsupported skeleton data, please export with a newer version of Spine.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 VSCode 编辑 &lt;code&gt;NP0172_spr.json&lt;/code&gt; 文件，将开头的 &lt;code&gt;spine&lt;/code&gt; 字段修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- {&quot;skeleton&quot;:{&quot;hash&quot;:&quot;yYnv88Uea00W/bB6gcmM/ZQmLSA&quot;,&quot;spine&quot;:&quot;3.8.75&quot;... }}
+ {&quot;skeleton&quot;:{&quot;hash&quot;:&quot;yYnv88Uea00W/bB6gcmM/ZQmLSA&quot;,&quot;spine&quot;:&quot;3.8.99&quot;... }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次尝试运行，这次可正常显示了。经测试，实际上可以将它改成任意 &lt;code&gt;3.8.x&lt;/code&gt;，只要不是 &lt;code&gt;3.8.75&lt;/code&gt; 即可。&lt;/p&gt;
&lt;h3&gt;8. 测量视口区域&lt;/h3&gt;
&lt;p&gt;为了避免动画切换时可能出现的轻微位移现象，可以预先在代码中固定好视口的位置和尺寸。虽然 &lt;code&gt;Idle_01&lt;/code&gt; 动画中的看板娘实际上会移动约 2.5 像素，但这一变化在视觉上很难被察觉。&lt;/p&gt;
&lt;p&gt;通过单击视口中缩放滑块上方的标尺按钮可以显示标尺。标尺单位采用世界坐标，与尚未缩放的图片的像素相对应。标尺上，每大格代表 500 像素。每个大格均分为 8 小格，故每小格代表 62.5 像素。&lt;/p&gt;
&lt;p&gt;初始化播放器时，为了自定义 &lt;code&gt;viewport&lt;/code&gt; 字段的值，需要用标尺去分别测量看板娘左下角处的位置和看板娘尺寸。这结果大概分别为 &lt;code&gt;(-300,-750)&lt;/code&gt; 和 &lt;code&gt;625×2150&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;viewport: {
  padLeft: 0,
  padRight: 0,
  padTop: 0,
  padBottom: 0,
  x: -300,
  y: -750,
  width: 625,
  height: 2150,
  debugRender: true,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9. 添加交互&lt;/h3&gt;
&lt;p&gt;使用 AnimationState 的 &lt;code&gt;setAnimation (	int trackIndex, Animation animation, bool loop): TrackEntry&lt;/code&gt; 方法可以实现动画分层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轨道 0：用于播放 &lt;code&gt;Idle_01&lt;/code&gt; 或眼睛和口型变化的动画&lt;/li&gt;
&lt;li&gt;轨道 1：持续循环播放 &lt;code&gt;halo_float&lt;/code&gt; 动画&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过分层处理后，动画效果更加自然。即使看板娘的表情和口型发生变化，其头顶光环的浮动位置也不会被重置。&lt;/p&gt;
&lt;p&gt;还有，看板娘眼睛和口型发生变化后，在经过一段时间（如 1500ms）后会恢复原状：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;showControls: false, // 隐藏控件
animation: &quot;Idle_01&quot;, // 初始播放动画
success: (player) =&amp;gt; {
  player.animationState.setAnimation(1, &quot;halo_float&quot;, true);

  isClickable = true;
  document.getElementById(&quot;player-container&quot;).addEventListener(&quot;click&quot;, () =&amp;gt; {
    if (!isClickable) return;

    isClickable = false;
    const animations = [&quot;00&quot;, &quot;01&quot;, &quot;02&quot;, &quot;04&quot;, &quot;05&quot;, &quot;06&quot;, &quot;07&quot;, &quot;08&quot;, &quot;99&quot;];
    const randomIndex = Math.floor(Math.random() * animations.length);
    const selectedAnimation = animations[randomIndex];
    player.animationState.setAnimation(0, selectedAnimation, true);
    setTimeout(() =&amp;gt; {
      player.animationState.setAnimation(0, &quot;Idle_01&quot;, false);
      isClickable = true;
    }, 1500);
  });
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，本 Demo 就实现完毕了。&lt;/p&gt;
&lt;h2&gt;修改源码&lt;/h2&gt;
&lt;p&gt;一旦将 Demo 实现出来后，就能够快速地修改 Firely 主题源码去实现之前提到的完整需求了。&lt;/p&gt;
&lt;h3&gt;1. 更换模型文件&lt;/h3&gt;
&lt;p&gt;Firefly 主题的 &lt;code&gt;public/pio/models/spine&lt;/code&gt; 目录存放着 Spine 看板娘的数据文件。删除该目录下的 &lt;code&gt;firefly&lt;/code&gt; 目录，然后新建一个名为 &lt;code&gt;shiroko&lt;/code&gt; 的目录，并将之前导出的资源文件复制到 &lt;code&gt;shiroko&lt;/code&gt; 目录中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spine
└── shiroko
    ├── NP0172_spr.atlas
    ├── NP0172_spr.json
    └── NP0172_spr.png
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 更换 JavaScript 和 CSS 文件&lt;/h3&gt;
&lt;p&gt;目前，Spine Web Player 所需的 JavaScript 和 CSS 文件存放在主题 &lt;code&gt;public/pio/static&lt;/code&gt; 目录中，对应文件名分别为 &lt;code&gt;spine-player.min.js&lt;/code&gt; 和 &lt;code&gt;spine-player.min.css&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当前 Firefly 主题源码引入的 Spine Web Player 版本为 4.2.x，而用于导出看板娘的 Spine 编辑器版本为 3.8.75。为保持版本兼容性，需要将相关文件替换为对应的 3.8 版本。&lt;/p&gt;
&lt;p&gt;操作时要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Spine Web Player 3.8 版本的官方 &lt;code&gt;spine-player.js&lt;/code&gt; 和 &lt;code&gt;spine.player.css&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;分别通过 &lt;a href=&quot;https://minify-js.com&quot;&gt;Minify JS Online&lt;/a&gt; 和 &lt;a href=&quot;https://minify-css.com&quot;&gt;Minify CSS Online&lt;/a&gt; 对代码进行压缩&lt;/li&gt;
&lt;li&gt;将压缩后的内容分别覆盖至现有的 &lt;code&gt;spine-player.min.js&lt;/code&gt; 和 &lt;code&gt;spine-player.min.css&lt;/code&gt; 文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 修改配置类型文件&lt;/h3&gt;
&lt;p&gt;由于主版本号不同，之间的 API 定义可能存在差异。此外，当前的看板娘配置类型定义也较为粗糙，有必要进一步优化调整。&lt;/p&gt;
&lt;p&gt;主题的配置类型都定义在 &lt;code&gt;src/types/config.ts&lt;/code&gt; 文件中。将 Spine 看板娘配置类型的定义修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Spine 看板娘配置
export type SpineModelConfig = {
  enable: boolean; // 是否启用 Spine 看板娘
  model: {
-    path: string; // 模型文件路径 (.json)
-    scale?: number; // 模型缩放比例，默认1.0
-    x?: number; // X轴偏移，默认0
-    y?: number; // Y轴偏移，默认0
-  };
+    path: string; // 模型文件路径 (.json) 或 (.skel)
+  };
+  atlas: {
+    path: string; // 模型纹理文件路径 (.atlas)
+  };
+  premultipliedAlpha?: boolean; // 是否使用预乘Alpha，默认true
  position: {
    corner: &quot;bottom-left&quot; | &quot;bottom-right&quot; | &quot;top-left&quot; | &quot;top-right&quot;; // 显示位置
    offsetX?: number; // 水平偏移量，默认20px
    offsetY?: number; // 垂直偏移量，默认20px
  };
-  size: {
+  size?: {
    width?: number; // 容器宽度，默认280px
    height?: number; // 容器高度，默认400px
-  };
+    zoom?: number; // 缩放比例，默认1.0
+  };
+  viewport?: {
+    x?: number; // 视口X坐标
+    y?: number; // 视口Y坐标
+    width?: number; // 视口宽度
+    height?: number; // 视口高度
+    padLeft?: string | number; // 视口左侧内边距
+    padRight?: string | number; // 视口右侧内边距
+    padTop?: string | number; // 视口上侧内边距
+    padBottom?: string | number; // 视口下侧内边距
+    debugRender?: boolean; // 是否启用调试渲染
+  };
+  animations: {
+    initial: string; // 初始动画
+    idle: string[]; // 待机动画列表
+    idleInterval?: number; // 待机动画切换间隔（毫秒），默认10000
+    persistent: string[]; // 持续播放的动画列表
+  },
  interactive?: {
    enabled?: boolean; // 是否启用交互功能，默认true
    clickAnimations?: string[]; // 点击时随机播放的动画列表
    clickMessages?: string[]; // 点击时随机显示的文字消息
    messageDisplayTime?: number; // 文字显示时间（毫秒），默认3000
-    idleAnimations?: string[]; // 待机动画列表
-    idleInterval?: number; // 待机动画切换间隔（毫秒），默认10000
  };
  responsive?: {
    hideOnMobile?: boolean; // 是否在移动端隐藏，默认false
    mobileBreakpoint?: number; // 移动端断点，默认768px
  };
  zIndex?: number; // 层级，默认1000
  opacity?: number; // 透明度，0-1，默认1.0
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入 &lt;code&gt;zoom&lt;/code&gt; 字段可以灵活调整看板娘容器的显示尺寸，且缩放效果相较于其他实现方式更为平滑流畅。&lt;/p&gt;
&lt;h3&gt;4. 修改配置文件&lt;/h3&gt;
&lt;p&gt;Spine 看板娘相关配置在 &lt;code&gt;src/config/pioConfig.ts&lt;/code&gt; 文件中，修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-// Spine 看板娘配置
+// Spine 看板娘配置（版本 3.8）
export const spineModelConfig: SpineModelConfig = {
-  enable: false, // 启用 Spine 看板娘
+  enable: true, // 启用 Spine 看板娘
  model: {
    // Spine模型文件路径
-    path: &quot;/pio/models/spine/firefly/1310.json&quot;,
-    scale: 1.0, // 模型缩放比例
-    x: 0, // X轴偏移
-    y: 0, // Y轴偏移
-  },
+    path: &quot;/pio/models/spine/shiroko/NP0172_spr.json&quot;,
+  },
+  atlas: {
+    // Spine模型纹理文件路径
+    path: &quot;/pio/models/spine/shiroko/NP0172_spr.atlas&quot;,
+  },
+  premultipliedAlpha: true, // 使用预乘Alpha
  position: {
    // 显示位置 bottom-left，bottom-right，top-left，top-right，注意：在右下角可能会挡住返回顶部按钮
    corner: &quot;bottom-left&quot;,
    offsetX: 0, // 距离右边缘0px
    offsetY: 0, // 距离底部0px
  },
  size: {
-    width: 135, // 容器宽度
-    height: 165, // 容器高度
+    width: 625, // 容器宽度
+    height: 2150, // 容器高度
+    zoom: 0.18, // 容器缩放比例
+  },
+  viewport: {
+    padLeft: 0,
+    padRight: 0,
+    padTop: 0,
+    padBottom: 0,
+    x: -300,
+    y: -750,
+    width: 625,
+    height: 2150,
+    debugRender: false,
+  },
+  animations: {
+    initial: &quot;Idle_01&quot;, // 初始动画
+    idle: [&quot;Idle_01&quot;], // 待机动画列表
+    idleInterval: 8000, // 待机动画切换间隔（8秒）
+    persistent: [&quot;halo_float&quot;] // 持续播放的动画列表
  },
  interactive: {
    enabled: true, // 启用交互功能
-    clickAnimations: [
-      &quot;emoji_0&quot;,
-      &quot;emoji_1&quot;,
-      &quot;emoji_2&quot;,
-      &quot;emoji_3&quot;,
-      &quot;emoji_4&quot;,
-      &quot;emoji_5&quot;,
-      &quot;emoji_6&quot;,
-    ], // 点击时随机播放的动画列表
-    clickMessages: [
-      &quot;你好呀！我是流萤~&quot;,
-      &quot;今天也要加油哦！✨&quot;,
-      &quot;想要一起去看星空吗？🌟&quot;,
-      &quot;记得要好好休息呢~&quot;,
-      &quot;有什么想对我说的吗？💫&quot;,
-      &quot;让我们一起探索未知的世界吧！🚀&quot;,
-      &quot;每一颗星星都有自己的故事~⭐&quot;,
-      &quot;希望能带给你温暖和快乐！💖&quot;,
+    clickAnimations: [&quot;00&quot;, &quot;01&quot;, &quot;02&quot;, &quot;04&quot;, &quot;05&quot;, &quot;06&quot;, &quot;07&quot;, &quot;08&quot;, &quot;99&quot;], // 点击时随机播放的动画列表
+    clickMessages: [ // ---
+      &quot;欢迎来到这个小小的空间~&quot;,
+      &quot;今天也请享受探索的乐趣吧！&quot;,
+      &quot;发现什么有趣的内容了吗？&quot;,
+      &quot;要喝杯茶休息一下吗？&quot;,
+      &quot;希望你能在这里找到需要的信息&quot;,
+      &quot;阳光正好，是个适合阅读的日子&quot;,
+      &quot;每个角落都藏着小小惊喜&quot;,
+      &quot;感谢你的到访，愿你有个美好的一天&quot;,
+      &quot;这里记录着思考和成长的痕迹&quot;,
+      &quot;慢慢来，享受这段时光&quot;,
    ], // 点击时随机显示的文字消息
    messageDisplayTime: 3000, // 文字显示时间（毫秒）
-    idleAnimations: [&quot;idle&quot;, &quot;emoji_0&quot;, &quot;emoji_1&quot;, &quot;emoji_3&quot;, &quot;emoji_4&quot;], // 待机动画列表
-    idleInterval: 8000, // 待机动画切换间隔（8秒）
  },
  responsive: {
    hideOnMobile: true, // 在移动端隐藏
    mobileBreakpoint: 768, // 移动端断点
  },
  zIndex: 1000, // 层级
  opacity: 1.0, // 完全不透明
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里也同时调整了点击看板娘时随机显示的文字消息内容。&lt;/p&gt;
&lt;h3&gt;5. 修改看板娘展示组件&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;src/components/widget/SpineModel.astro&lt;/code&gt; 文件为看板娘的核心展示组件。根据新版配置做适配修改，以确保所有配置项能够正确应用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改点 1：调整 HTML 元素属性&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Spine Web Player CSS 将在 script 中动态加载 --&amp;gt;{
  spineModelConfig.enable &amp;amp;&amp;amp; (
    &amp;lt;div
+      id=&quot;spine-model-container&quot;
+      style={`
+      position: fixed;
+      ${spineModelConfig.position.corner.includes(&quot;right&quot;) ? &quot;right&quot; : &quot;left&quot;}: ${spineModelConfig.position.offsetX}px;
+      ${spineModelConfig.position.corner.includes(&quot;top&quot;) ? &quot;top&quot; : &quot;bottom&quot;}: ${spineModelConfig.position.offsetY}px;
-      width: ${spineModelConfig.size.width}px;
-      height: ${spineModelConfig.size.height}px;
      pointer-events: auto;
      z-index: 1000;
    `}
    &amp;gt;
-      &amp;lt;div id=&quot;spine-player-container&quot; style=&quot;width: 100%; height: 100%;&quot; /&amp;gt;
+      &amp;lt;div
+        id=&quot;spine-player-container&quot;
+        style={`
+          width: ${spineModelConfig?.size?.width || 280}px;
+          height: ${spineModelConfig?.size?.height || 400}px;
+          zoom: ${spineModelConfig?.size?.zoom || 1};
+        `} /&amp;gt;
      &amp;lt;div id=&quot;spine-error&quot; style=&quot;display: none;&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改点 2：更正 Altas 路径&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- &amp;lt;script define:vars={{ spineModelConfig, modelPath: url(spineModelConfig.model.path), atlasPath: url(spineModelConfig.model.path.replace(&quot;.json&quot;, &quot;.atlas&quot;)), cssPath: url(&quot;/pio/static/spine-player.min.css&quot;), jsPath: url(&quot;/pio/static/spine-player.min.js&quot;) }}&amp;gt;
+ &amp;lt;script define:vars={{ spineModelConfig, modelPath: url(spineModelConfig.model.path), atlasPath: url(spineModelConfig.atlas.path), cssPath: url(&quot;/pio/static/spine-player.min.css&quot;), jsPath: url(&quot;/pio/static/spine-player.min.js&quot;) }}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改点 3：修改 CDN 链接&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;源代码目前优先尝试从 CDN 获取 Spine Web Player 4.2 版本的 JavaScript 和 CSS 文件；若获取失败，则回退到本地服务器资源。由于目前尚未找到 3.8 版本的公开 CDN 地址，为了简化修改并避免大规模调整代码，我将原 CDN 链接直接替换为指向本地服务器的相同文件路径。这样，将始终从本地加载 3.8 版本的文件，虽然可能在回退逻辑下触发两次本地请求，但整体逻辑保持简洁、易于维护。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- cdnLink.href =
-   &quot;https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/spine-player.min.css&quot;;
+ cdnLink.href = cssPath;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- script.src =
-   &quot;https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.min.js&quot;;
+ script.src = jsPath;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改点 4：调整初始化播放器的参数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建 SpinePlayer
const player = new window.spine.SpinePlayer(&quot;spine-player-container&quot;, {
-  skeleton: modelPath,
-  atlas: atlasPath,
-  animation: &quot;idle&quot;,
+  jsonUrl: modelPath.endsWith(&quot;.json&quot;) ? modelPath : null,
+  skelUrl: modelPath.endsWith(&quot;.skel&quot;) ? modelPath : null,
+  atlasUrl: atlasPath,
+  animation: spineModelConfig.animations.initial,
  backgroundColor: &quot;#00000000&quot;, // 透明背景
  showControls: false, // 隐藏控件
  alpha: true,
-  premultipliedAlpha: false,
+  premultipliedAlpha: spineModelConfig.premultipliedAlpha || true,
+  viewport: spineModelConfig.viewport,
  /* ... */
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修改点 5：调整动画逻辑&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;目前，看板娘的动画播放是基于单轨道机制实现的。为了支持动画分层播放功能，接下来需要将代码改成基于多轨道的架构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;轨道 0（主动画层）：用于循环播放待机动画和响应点击交互&lt;/li&gt;
&lt;li&gt;轨道 1 及以上（附加动画层）：持续播放背景或装饰性动画，实现动画叠加效果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一调整将实现与之前 Demo 版本一致的分层动画效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;success: (player) =&amp;gt; {
  console.log(&quot;🎉 Spine model loaded successfully!&quot;);

  // 保存播放器实例引用
  window.spinePlayerInstance = player;
+  
+  // 播放持续动画
+  spineModelConfig.animations.persistent.forEach((anim, i) =&amp;gt; {
+    try {
+      player.animationState.setAnimation(i + 1, anim, true);
+    } catch (e) {
+      console.warn(&quot;Failed to play persistent animation:&quot;, e);
+    }
+  });

  // 初始化完成后设置默认姿态
  setTimeout(() =&amp;gt; {
    if (player.skeleton) {
      try {
        player.skeleton.updateWorldTransform();
        player.skeleton.setToSetupPose();
      } catch (e) {
        console.warn(&quot;Error positioning skeleton:&quot;, e);
      }
    }
  }, 500);
+
+  // 设置待机动画循环
+  if (spineModelConfig.animations.idle.length &amp;gt; 1) {
+    setInterval(() =&amp;gt; {
+      try {
+        const idleAnims =
+          spineModelConfig.animations.idle;
+        const randomIdle =
+          idleAnims[Math.floor(Math.random() * idleAnims.length)];
+        player.animationState.setAnimation(0, randomIdle, true);
+      } catch (e) {
+        console.warn(&quot;Failed to play idle animation:&quot;, e);
+      }
+    }, spineModelConfig.animations.idleInterval || 10000);
+  }

  // 设置交互功能
  if (spineModelConfig.interactive.enabled) {
    const canvas = document.querySelector(
      &quot;#spine-player-container canvas&quot;
    );
    if (canvas) {
      canvas.addEventListener(&quot;click&quot;, () =&amp;gt; {
        // 防抖处理：防止重复点击
        const currentTime = Date.now();
        if (isClickProcessing || currentTime - lastClickTime &amp;lt; 500) {
          return; // 500ms 内重复点击忽略
        }

        isClickProcessing = true;
        lastClickTime = currentTime;

        // 随机播放点击动画
-        const clickAnims =
-          spineModelConfig.interactive.clickAnimations ||
-          (spineModelConfig.interactive.clickAnimation
-            ? [spineModelConfig.interactive.clickAnimation]
-            : []);
+        const clickAnims = spineModelConfig.interactive.clickAnimations

        if (clickAnims.length &amp;gt; 0) {
          try {
            const randomClickAnim =
              clickAnims[Math.floor(Math.random() * clickAnims.length)];
-            player.setAnimation(randomClickAnim, false);
+            player.animationState.setAnimation(0, randomClickAnim, false);

            // 动画播放完成后回到待机状态
            setTimeout(() =&amp;gt; {
              const idleAnims =
-                spineModelConfig.interactive.idleAnimations;
+                spineModelConfig.animations.idle;
              const randomIdle =
                idleAnims[Math.floor(Math.random() * idleAnims.length)];
-              player.setAnimation(randomIdle, true);
+              player.animationState.setAnimation(0, randomIdle, true);
            }, 2000);
          } catch (e) {
            console.warn(&quot;Failed to play click animation:&quot;, e);
          }
        }

        // 显示随机消息
        const messages = spineModelConfig.interactive.clickMessages;
        if (messages &amp;amp;&amp;amp; messages.length &amp;gt; 0) {
          const randomMessage =
            messages[Math.floor(Math.random() * messages.length)];
          showMessage(randomMessage);
        }

        // 500ms 后重置防抖标志
        setTimeout(() =&amp;gt; {
          isClickProcessing = false;
        }, 500);
      });
-
-      // 设置待机动画循环
-      if (spineModelConfig.interactive.idleAnimations.length &amp;gt; 1) {
-        setInterval(() =&amp;gt; {
-          try {
-            const idleAnims =
-              spineModelConfig.interactive.idleAnimations;
-            const randomIdle =
-              idleAnims[Math.floor(Math.random() * idleAnims.length)];
-            player.setAnimation(randomIdle, true);
-          } catch (e) {
-            console.warn(&quot;Failed to play idle animation:&quot;, e);
-          }
-        }, spineModelConfig.interactive.idleInterval);
-      }
    }
  }

  /* ... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，访问 &lt;code&gt;http://localhost:3000/&lt;/code&gt;，测试并确认各项功能正常。&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;h3&gt;提交更改并推送至线上&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git rm -r public/pio/models/spine/firefly
git add public/pio/models/spine/shiroko
git add public/pio/static/spine-player.min.css
git add public/pio/static/spine-player.min.js
git add src/components/widget/SpineModel.astro
git add src/config/pioConfig.ts
git add src/types/config.ts
git commit -m &quot;chore: 更换看板娘为 shiroko 并适配 Spine Web Player 3.8&quot; -m &quot;- 删除原看板娘 firefly，新增 看板娘 shiroko&quot; -m &quot;- 将 Spine Web Player 从 4.2 替换为 3.8&quot; -m &quot;- 调整看板娘配置类型定义&quot; -m &quot;- 更新看板娘配置项&quot; -m &quot;- 修改看板娘展示组件逻辑&quot; -m &quot;- 启用看板娘功能&quot;
git push origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;踩坑复盘&lt;/h3&gt;
&lt;p&gt;折腾了几天，新的看板娘终于完美地站在了我的博客角落。本以为就是“换个看板娘”这么简单，结果却耗在寻找更合适的看板娘、调试代码、以及反复踩坑上。为了跳出这些坑，我不得不学着使用各种新工具、理解一堆陌生概念，在尝试、失败、再尝试的循环里打转了好一阵子。&lt;/p&gt;
&lt;h4&gt;踩坑过程&lt;/h4&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;查找对应版本的运行时库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;找不到稳定 CDN&lt;/td&gt;
&lt;td&gt;官方未提供旧版运行时库的 CDN&lt;/td&gt;
&lt;td&gt;下载文件到本地，之后跟着部署到 GitHub Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仍然打不开骨骼数据文件&lt;/td&gt;
&lt;td&gt;调用 API 时传入的字段名称不正确&lt;/td&gt;
&lt;td&gt;根据正确版本的 API 定义填入正确的字段名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成功打开骨骼数据文件，但脸部显示异常&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;尝试禁用预乘 Alpha&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;重新启用预乘 Alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;脸部显示仍然异常&lt;/td&gt;
&lt;td&gt;导出资源时未启用预乘 Alpha&lt;/td&gt;
&lt;td&gt;下载 Spine 编辑器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spine Trail 打不开骨骼数据文件&lt;/td&gt;
&lt;td&gt;Spine 编辑器版本不合适&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;Spine Web Player 打开刚导出的资源时提示版本不支持&lt;/td&gt;
&lt;td&gt;版本不正确&lt;/td&gt;
&lt;td&gt;尝试修改 JSON 格式的图集文件中的 spine 字段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;看板娘显示成功，但在切换动画时她有轻微的上下位移&lt;/td&gt;
&lt;td&gt;视口未固定&lt;/td&gt;
&lt;td&gt;测量视口并正确配置 viewport 字段&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;code&gt;zoom&lt;/code&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;别看上面写的“推测原因”和“行动”好像很准确，其实这些几乎都已经是整理后的“正确路径”了。当时真正的情况是：面对每个问题，我经常毫无头绪，甚至跑错方向。问了 AI，给的答案也常常不到位；最后只能迷茫地在网上搜来搜去，尝试各种可能根本不靠谱的方法。&lt;/p&gt;
&lt;h4&gt;踩坑教训&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;版本兼容性很重要：编辑器、运行时库、数据三者的版本必须严格兼容&lt;/li&gt;
&lt;li&gt;警惕 API 的静默变更：不同版本间接口或字段可能存在差异，查阅文档时需锁定对应版本，而非仅参考最新&lt;/li&gt;
&lt;li&gt;陌生概念是解题的钥匙：主动理解预乘 Alpha、轨道等核心概念，能从根本上定位问题，而非盲目尝试&lt;/li&gt;
&lt;li&gt;领域经验是解题的“直觉库”：在特定领域（如 2D 模型处理）积累的经验，能在遇到新问题时，更快地定位方向、形成假设，少走许多弯路&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>基于 Firefly 主题的 Astro 博客部署（四）：giscus 评论系统配置</title><link>https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part4/</link><guid isPermaLink="true">https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part4/</guid><description>记录了自己在基于 Firefly 主题的 Astro 博客上配置启用 giscus 评论系统，并简单总结了该评论系统的部分流程。</description><pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;站点经过批量配置更新后，如今已颇具辨识度。尽管整体结构仍保留着 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot;&gt;&lt;strong&gt;Firefly&lt;/strong&gt;&lt;/a&gt; 主题的影子，但最起码能让访客一眼将它与其他博客区分开来。&lt;/p&gt;
&lt;p&gt;目前，访客在站内仅能通过点击链接跳转页面、阅读内容。尚缺少一个公开的交流渠道，让访客与访客、博主与访客之间能够直接对话，引入评论系统势在必行。虽然之前设置过 Email 地址、&lt;strong&gt;GitHub&lt;/strong&gt; 链接等，但这些渠道不够直接，也缺乏公开讨论的氛围。要知道，有时评论的价值甚至超越文章本身，沉淀下来的评论也能为后来的访客提供宝贵的线索与回顾。&lt;/p&gt;
&lt;h2&gt;选择&lt;/h2&gt;
&lt;p&gt;回想我最初对博客部署的核心需求：快速开始、无需支付服务器和域名的定期费用、界面风格现代，外观精美、可定制化。这些需求也同样延伸到了对评论系统的考量上，并进一步考虑到第三方登录、数据所有权、浏览量统计等方面。&lt;/p&gt;
&lt;p&gt;Firefly 主题内置了四种供选择使用的评论系统：&lt;a href=&quot;https://twikoo.js.org&quot;&gt;&lt;strong&gt;Twikoo&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://waline.js.org&quot;&gt;&lt;strong&gt;Waline&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://giscus.app&quot;&gt;&lt;strong&gt;giscus&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://disqus.com&quot;&gt;&lt;strong&gt;Disqus&lt;/strong&gt;&lt;/a&gt;。经过调研，得到下面对比结果：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;需求&lt;/th&gt;
&lt;th&gt;Twikoo&lt;/th&gt;
&lt;th&gt;Waline&lt;/th&gt;
&lt;th&gt;giscus&lt;/th&gt;
&lt;th&gt;Disqus&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;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;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;⭐⭐⭐ 原生 GitHub 风格&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;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;⭐⭐ 仅支持 GitHub 登录&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;⭐⭐⭐ 自己掌握数据在 GitHub 仓库中，公开透明&lt;/td&gt;
&lt;td&gt;⭐⭐ 归属于 Disqus&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;td&gt;⭐⭐⭐⭐ 提供基础访问分析，但含广告追踪&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;综上来看，可以给出选择依据了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不选 Twikoo：如果不想自行部署或者注重身份识别&lt;/li&gt;
&lt;li&gt;不选 Waline：如果不想自行部署或者不注重身份识别&lt;/li&gt;
&lt;li&gt;不选 GitHub：如果希望完全自己掌控或者需要除 GitHub 以外的第三方登录方式&lt;/li&gt;
&lt;li&gt;不选 Disqus：如果希望自己完全掌控或者无法接受页面中嵌入广告&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于刚才提出的需求，这四种评论系统其实都基本满足，但在逐一权衡后，我采用了排除法进行决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disqus：因免费版包含广告，首先排除&lt;/li&gt;
&lt;li&gt;Twikoo：希望强制评论者进行身份验证，因此排除&lt;/li&gt;
&lt;li&gt;Waline：虽然功能强大，但我对免费数据库的数据持久性和请求延迟存疑。相比之下，没有任何免费平台比 GitHub 更能让我对数据安全放心，因此忍痛割舍&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终，考虑到我已经将博客部署于 GitHub Pages，那就索性地用 giscus 好了。虽然浏览量统计的功能，我还是希望有的，但胜在数据安全与深度集成。若未来迁移到云平台上部署，届时再考虑切换至 Waline。&lt;/p&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;按照 giscus 主页的说明一步一步地操作，并结合 Firefly 主题代码来修改就好。由于 Firefly 主题已经集成好评论系统，所以使用起来很简单。&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;简单来说，giscus 利用 GitHub Discussions 作为评价系统的后端，通过 &lt;strong&gt;GitHub App&lt;/strong&gt; 和前端嵌入脚本，在静态网站上实现动态评论功能。&lt;/p&gt;
&lt;p&gt;下面是它的工作原理分解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;strong&gt;GitHub Discussions&lt;/strong&gt; 作为评论系统的后端（需要手动在仓库设置里开启）&lt;/li&gt;
&lt;li&gt;仓库维护者通过安装 &lt;a href=&quot;https://github.com/apps/giscus&quot;&gt;&lt;strong&gt;giscus App&lt;/strong&gt;&lt;/a&gt;，授予它能读写指定的公开仓库的 Discussions 的权限&lt;/li&gt;
&lt;li&gt;在前端借助 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 标签嵌入 giscus 提供的脚本并传入参数，作用是：
&lt;ul&gt;
&lt;li&gt;giscus 加载时会使用 &lt;a href=&quot;https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions&quot;&gt;&lt;strong&gt;GitHub Discussions 搜索 API&lt;/strong&gt;&lt;/a&gt; 根据选定的映射方式（如 URL、&lt;code&gt;pathname&lt;/code&gt;、&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; 等）来查找与当前页面关联的 discussion。如果找不到匹配的 discussion，giscus 就会在第一次有人留下评论或回应时自动创建一个 discussion&lt;/li&gt;
&lt;li&gt;动态加载评论界面&lt;/li&gt;
&lt;li&gt;向 giscus 服务发起请求，带上当前页面的标识&lt;/li&gt;
&lt;li&gt;将 discussion 的评论内容渲染到页面上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;访客需要 GitHub 账号，按照 &lt;strong&gt;OAuth&lt;/strong&gt; 流程登录，授予 giscus App 代表他发布评论的权限（不过从源码可知，实际是 giscus 前端代表访客发布评论的），因为 GitHub 的写操作 API（如创建评论）必须携带访客 OAuth token&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此可见，这过程依赖第三方服务（giscus.app）。由于它是开源的，所以也可以自建。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;h4&gt;1. 启用 GitHub Discussions&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; （将 &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; 替换为 GitHub 用户名，下文同理）仓库页面下，点击顶部导航栏的 &lt;code&gt;Settings&lt;/code&gt; → 向下滚动页面至 &lt;code&gt;Features&lt;/code&gt; 部分 → 勾选 &lt;code&gt;Discussions&lt;/code&gt; 选项，开启仓库的 Discussions 功能。&lt;/p&gt;
&lt;h4&gt;2. 安装 giscus GitHub App&lt;/h4&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://github.com/apps/giscus&quot;&gt;GitHub App - giscus&lt;/a&gt; → 点击页面右侧栏的 &lt;code&gt;Install&lt;/code&gt; 按钮 → 选择 &lt;code&gt;Only select repositories&lt;/code&gt; → 点击 &lt;code&gt;Select repositories&lt;/code&gt; 下拉列表 → 选择 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; 项 → 点击 &lt;code&gt;Install&lt;/code&gt; 按钮 → 输入登录密码，确认权限。&lt;/p&gt;
&lt;h4&gt;3. 修改配置文件&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;src/config/commentConfig.ts&lt;/code&gt; 文件存放着关于评论系统的配置。先配置启用 &lt;code&gt;giscus&lt;/code&gt; 评论系统，然后依次设置 &lt;code&gt;repo&lt;/code&gt;、&lt;code&gt;repoId&lt;/code&gt;、&lt;code&gt;category&lt;/code&gt;、&lt;code&gt;categoryId&lt;/code&gt; 字段，其中 &lt;code&gt;category&lt;/code&gt; 字段选择 &lt;code&gt;Announcements&lt;/code&gt;（只有仓库的维护者和 giscus App 本身可以在“Announcements”分类中创建新的 discussion），其他字段根据代码注释去设置。建议先在 giscus 主页上配置下，再将配置的值填入到代码中。&lt;/p&gt;
&lt;p&gt;我将文件内容改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { CommentConfig } from &quot;../types/config&quot;;

export const commentConfig: CommentConfig = {
-  type: &apos;none&apos;, // 当前启用的评论系统类型: none, twikoo, waline, giscus, disqus，默认为none，即不启用评论系统。
+  type: &apos;giscus&apos;, // 当前启用的评论系统类型: none, twikoo, waline, giscus, disqus，默认为none，即不启用评论系统。
  // ...
  //giscus评论系统配置（还未测试）
  giscus: {
-    repo: &apos;CuteLeaf/Firefly&apos;, // 设置 Giscus 评论系统仓库
-    repoId: &apos;R_kgD2gfdFGd&apos;, // 设置 Giscus 评论系统仓库ID
-    category: &apos;General&apos;, // 设置 Giscus 评论系统分类
-    categoryId: &apos;DIC_kwDOKy9HOc4CegmW&apos;, // 设置 Giscus 评论系统分类ID
+    repo: &apos;xpfxzxc/xpfxzxc.github.io&apos;, // 设置 Giscus 评论系统仓库
+    repoId: &apos;R_kgDOQIvG_Q&apos;, // 设置 Giscus 评论系统仓库ID
+    category: &apos;Announcements&apos;, // 设置 Giscus 评论系统分类
+    categoryId: &apos;DIC_kwDOQIvG_c4Cxnv0&apos;, // 设置 Giscus 评论系统分类ID
    mapping: &apos;title&apos;, // 设置 Giscus 评论系统映射方式
    strict: &apos;0&apos;, // 设置 Giscus 评论系统严格模式
    reactionsEnabled: &apos;1&apos;, // 设置 Giscus 评论系统反应功能
    emitMetadata: &apos;1&apos;, // 设置 Giscus 评论系统元数据
    inputPosition: &apos;top&apos;, // 设置 Giscus 评论系统输入位置
    theme: &apos;light&apos;, // 设置 Giscus 评论系统主题
    lang: &apos;zh-CN&apos;, // 设置 Giscus 评论系统语言
    loading: &apos;lazy&apos;, // 设置 Giscus 评论系统加载方式
  },
  // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;提交更改并推送至线上&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/commentConfig.ts
git commit -m &quot;feat: 启用 giscus 评论系统&quot; -m &quot;- 启用评论功能，类型设置为 giscus&quot; -m &quot;- 配置个人 GitHub 仓库作为评论存储&quot; -m &quot;- 设置分类等交互选项&quot;
git push origin custom
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;扩展内容：OAuth 2.0&lt;/h2&gt;
&lt;p&gt;在博客页面下，若想通过使用 API 向 GitHub 提交评论至相应的 discussion，必须携带 GitHub 账号登录凭证或者授权令牌。否则 GitHub 将拒绝该请求，这意味着无法匿名评论 —— 每一条评论都需以某一 GitHub 用户的身份发布。&lt;/p&gt;
&lt;p&gt;从访客的角度来看，在第三方博客页面直接输入 GitHub 账号和密码，通常存在安全风险，一般访客会对此有所顾虑。此外，受浏览器&lt;strong&gt;同源策略&lt;/strong&gt;的限制，博客网站无法直接获取到 GitHub 域下的凭证信息，哪怕访客当前已在 GitHub 上保持登录状态。&lt;/p&gt;
&lt;h3&gt;目的&lt;/h3&gt;
&lt;p&gt;OAuth 2.0 就是为了解决上述问题而生的！它允许&lt;strong&gt;资源所有者&lt;/strong&gt;（访客）在不分享密码的情况下，允许&lt;strong&gt;授权服务器&lt;/strong&gt;（GitHub）授权一个第三方应用（&lt;strong&gt;客户端&lt;/strong&gt;，giscus App）有限地访问存储在另一个服务（&lt;strong&gt;资源服务器&lt;/strong&gt;，GitHub）上的资源。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
虽说可以让博客站点成为类似 giscus 这样“第三方应用”的存在，从而不需要 giscus 服务，但是这增加了维护后端的负担。而且，访客是否愿意信任博客站点，这也是一个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OAuth 2.0 里有 4 种授权模式，这里只讲解&lt;strong&gt;授权码模式&lt;/strong&gt;，该模式是最安全、最常用，适用于有后端的 Web 应用。&lt;/p&gt;
&lt;h3&gt;授权码模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;流程：
&lt;ol&gt;
&lt;li&gt;用户访问客户端，客户端将用户重定向到授权服务器&lt;/li&gt;
&lt;li&gt;用户在授权服务器上登录并同意授权&lt;/li&gt;
&lt;li&gt;授权服务器将用户重定向回客户端事先指定的地址（回调地址），并在 URL 中带上一个授权码&lt;/li&gt;
&lt;li&gt;客户端的后端服务器用这个授权码，连同自己的客户端 ID 和密钥，向授权服务器请求访问令牌&lt;/li&gt;
&lt;li&gt;授权服务器验证通过后，返回访问令牌（和可选的刷新令牌）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;优点：访问令牌不会暴露给前端浏览器，非常安全&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
在实际过程中，通信时携带的数据，需查阅相关文档以确定哪些参数是必需或可选的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例子&lt;/h3&gt;
&lt;p&gt;在静态博客站点中借助第三方 giscus App 实现在仓库的 Discussions 发布评论，由于静态博客站点本身不具备后端服务且未注册为 GitHub App，同时还需引入第三方 giscus App 作为中介，整个授权和发布流程会因此变得相对复杂。&lt;/p&gt;
&lt;p&gt;下面将结合博客站点、giscus App 和 giscus 代码片段具体讲解整个过程，开始前有准备工作。&lt;/p&gt;
&lt;h4&gt;将 giscus 注册为 GitHub App&lt;/h4&gt;
&lt;p&gt;（这步 giscus 已经做了）先部署 giscus，然后要在 &lt;a href=&quot;https://github.com/settings/apps/new&quot;&gt;Register new GitHub App&lt;/a&gt; 页面上将 giscus 注册为 GitHub App，并按照要求填入信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GitHub App name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Description&lt;/code&gt;（可选）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Homepage URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Callback URL&lt;/code&gt;：使用 &lt;code&gt;https://[YOUR-DOMAIN-HERE]/api/oauth/authorized&lt;/code&gt; 作为授权回调 URL，例如：&lt;code&gt;https://giscus.app/api/oauth/authorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expire user authorization tokens&lt;/code&gt;：不勾选，目前 giscus 不支持它&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Request user authorization (OAuth) during installation&lt;/code&gt;：不勾选&lt;/li&gt;
&lt;li&gt;Webhook 部分的 &lt;code&gt;Active&lt;/code&gt;：不勾选&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Repository permissions&lt;/code&gt;：选择 &lt;code&gt;Discussions&lt;/code&gt; 的 &lt;code&gt;Access: Read &amp;amp; write&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;点击 &lt;code&gt;Create GitHub App&lt;/code&gt; 按钮后，将 giscus 注册为 GitHub App，以后称呼它为 giscus App。&lt;/p&gt;
&lt;p&gt;一旦创建完成，就要点击 &lt;code&gt;Generate a new client secret&lt;/code&gt; 和 &lt;code&gt;Generate a private key&lt;/code&gt; 按钮分别创建客户端密钥和私钥，其中私钥会自动被下载下来。&lt;/p&gt;
&lt;p&gt;上面的 &lt;code&gt;Callback URL&lt;/code&gt;、&lt;code&gt;App ID&lt;/code&gt;、&lt;code&gt;Client ID&lt;/code&gt;、&lt;code&gt;Client secret&lt;/code&gt;、&lt;code&gt;Private key&lt;/code&gt; 后面都会用到的。&lt;/p&gt;
&lt;h4&gt;为仓库安装 giscus App&lt;/h4&gt;
&lt;p&gt;在目标仓库中完成该 GitHub App 的授权安装后，GitHub 会为此次安装分配一个唯一的 &lt;strong&gt;installation ID&lt;/strong&gt;。giscus 服务端后续可凭此 ID 换取一个 &lt;strong&gt;installation access token&lt;/strong&gt;，从而获得以 App 身份在仓库中创建“Announcements”类型 discussion 的权限：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;giscus 服务端会用 App 的私钥生成一个 &lt;strong&gt;JWT&lt;/strong&gt; 用于证明“我是这个 GitHub App”&lt;/li&gt;
&lt;li&gt;giscus 服务端接着携带该 &lt;code&gt;installation ID&lt;/code&gt; 和 &lt;code&gt;JWT&lt;/code&gt;，调用 GitHub API 请求安装令牌&lt;/li&gt;
&lt;li&gt;经 GitHub 验证 JWT 后，服务端会得到一个 &lt;code&gt;installation access token&lt;/code&gt;，这个令牌带有仓库级别的权限（比如 Discussions 的读写）&lt;/li&gt;
&lt;li&gt;giscus 服务端缓存它，等到需要的时候再使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接着，启用该仓库的 Discussions 功能，并在前端页面插入脚本并配置好后，访客就可以正常使用 giscus 评论系统了。&lt;/p&gt;
&lt;h4&gt;授权和使用流程&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;第一步：博客站点引导访客至 GitHub 授权端点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当访客在博客文章页面里点击 giscus 界面上的 &lt;code&gt;使用 GitHub 登录&lt;/code&gt; 按钮后，浏览器将跳转至 giscus 的授权请求端点。此端点 URL 中已通过 &lt;code&gt;redirect_url&lt;/code&gt; 参数指定了回调地址，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://giscus.app/api/oauth/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A4321%2Fposts%2Fastro-firefly-blog-deploy-part4%2F%23comments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着，授权流程结束后，访客将被带回初始的文章页面（&lt;code&gt;http://localhost:4321/posts/astro-firefly-blog-deploy-part4/#comments&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;giscus App 会处理该请求，根据源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { NextApiRequest, NextApiResponse } from &apos;next&apos;;
import { encodeState } from &apos;../../../lib/oauth/state&apos;;
import { env } from &apos;../../../lib/variables&apos;;

const GITHUB_OAUTH_AUTHORIZE_URL = &apos;https://github.com/login/oauth/authorize&apos;;

export default async function OAuthAuthorizeApi(req: NextApiRequest, res: NextApiResponse) {
  const appReturnUrl = req.query.redirect_uri as string;

  if (!appReturnUrl) {
    res.status(400).json({ error: &apos;`redirect_uri` is required.&apos; });
    return;
  }

  const { client_id } = env;
  const proto = req.headers[&apos;x-forwarded-proto&apos;] || &apos;http&apos;;
  const redirect_uri = `${proto}://${req.headers.host}/api/oauth/authorized`;
  const state = await encodeState(appReturnUrl, env.encryption_password);

  const oauthParams = new URLSearchParams({ client_id, redirect_uri, state });
  res.redirect(302, `${GITHUB_OAUTH_AUTHORIZE_URL}?${oauthParams}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;giscus 服务端会立即构造一个特殊的 GitHub URL 并将访客的浏览器重定向过去。&lt;/p&gt;
&lt;p&gt;例如，URL 和参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET https://github.com/login/oauth/authorize?
  client_id=Iv1.c654bd032df6a55f&amp;amp;
  redirect_uri=https://giscus.app/api/oauth/authorized&amp;amp;
  state=032e2710f1484e164291ddferX/0OIIoT+b4Dsmy6hhD6QimL9cdx4EN2PZdmQGrLBwfCGbC5n6pEL9mJVYEGIiakJc8ZSd34wtSoxebcDhqV2IIryat1TK+D89AK37hQyi/tDvko5swfzlYgnlMZ2qOvNgZeFqM2/Bl64oJkyc+xo0OkVNB6r5vpA==
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数讲解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client_id&lt;/code&gt;：giscus App 的唯一标识符。这个就是在注册为 GitHub App 后得到的 &lt;code&gt;Client ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirect_uri&lt;/code&gt;：回调地址。这是将 giscus 注册为 GitHub App 时预先填写的 &lt;code&gt;Callback URL&lt;/code&gt;。授权成功后，GitHub 会把访客（和授权码）送回这个地址。这是安全的关键，防止授权码被发送到任意地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;：一个随机的、不可猜测的字符串。从上面 giscus 源码片段可知，它将重定向地址（博客网页地址）进行了加密并作为 &lt;code&gt;state&lt;/code&gt; 的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第二步：访客在 GitHub 上进行认证和授权&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访客的浏览器被重定向到 &lt;code&gt;https://github.com/login/oauth/authorize?...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果访客未登录 GitHub，会看到登录页面，访客需要输入 GitHub 的用户名和密码。&amp;lt;strong&amp;gt;注意：&amp;lt;/strong&amp;gt;密码是输给 GitHub 的，博客站点完全看不到&lt;/li&gt;
&lt;li&gt;登录成功后，GitHub 会显示一个授权页面，询问访客：“Giscus by Giscus 希望获得以下许可...”，并列出请求的权限&lt;/li&gt;
&lt;li&gt;访客点击绿色的 &lt;code&gt;Authorize giscus&lt;/code&gt; 按钮&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
还有一些情况是：重定向后，需要从多个账号中选择；或者，之前已经授权过，不需要再授权。但不管怎么样，授权过程类似。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;第三步：GitHub 重定向回 giscus App 并携带授权码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;访客点击授权后，GitHub 的授权服务器会生成一个短期有效的授权码，然后将访客的浏览器重定向到 giscus App 预先注册的回调地址（与第一步中 giscus 服务端指定的 &lt;code&gt;redirect_uri&lt;/code&gt; 参数一致），即 &lt;code&gt;https://giscus.app/api/oauth/authorized&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;URL 和参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET https://giscus.app/api/oauth/authorized?
  code=604a74dcc85e661b7a9b&amp;amp;
  state=c036727482c11062ed961334%2FdS8DSACyyCYlAhLeDC1cK1jhxrjUGctAQQDiSm6mdevw1crmOPggFYjkPBH2cpuP5ZhHtuB5VFvmnYPJ3x6Fv%2B9YoynA667JiUnaHx6iaULJtOlH%2Bzodau0S4pVQq19p%2BigTYnhXZ%2BsyaHLhIin0rBX2w%2BeBflD9A%3D%3D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数讲解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt;：授权码。它是一个代表用户同意的凭证，但本身不能用来访问 API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;：原样返回第一步中传来的 &lt;code&gt;state&lt;/code&gt; 参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第四步：giscus App 用授权码向 GitHub 兑换访问令牌&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;giscus App 在 &lt;code&gt;/api/oauth/authorized&lt;/code&gt; 端点处理回调：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { NextApiRequest, NextApiResponse } from &apos;next&apos;;
import { encodeState, decodeState } from &apos;../../../lib/oauth/state&apos;;
import { env } from &apos;../../../lib/variables&apos;;

const GITHUB_OAUTH_ACCESS_TOKEN_URL = &apos;https://github.com/login/oauth/access_token&apos;;
const TOKEN_VALIDITY_PERIOD = 1000 * 60 * 60 * 24 * 365; // 1 year;

export default async function OAuthAuthorizedApi(req: NextApiRequest, res: NextApiResponse) {
  const code = req.query.code as string;
  const state = req.query.state as string;
  const error = req.query.error as string;

  const { client_id, client_secret, encryption_password } = env;

  let appReturnUrl: string;
  try {
    appReturnUrl = await decodeState(state, encryption_password);
  } catch (err) {
    res.status(400).json({ error: err.message });
    return;
  }

  const returnUrl = new URL(appReturnUrl);

  if (error &amp;amp;&amp;amp; error === &apos;access_denied&apos;) {
    res.redirect(302, returnUrl.href);
    return;
  }

  if (!code || !state) {
    res.status(400).json({ error: &apos;`code` and `state` are required.&apos; });
    return;
  }

  const init = {
    method: &apos;POST&apos;,
    body: new URLSearchParams({ client_id, client_secret, code, state }),
    headers: {
      Accept: &apos;application/json&apos;,
      &apos;User-Agent&apos;: &apos;giscus&apos;,
    },
  };

  let accessToken: string;
  try {
    const response = await fetch(GITHUB_OAUTH_ACCESS_TOKEN_URL, init);
    if (response.ok) {
      const data = await response.json();
      accessToken = data.access_token;
    } else {
      throw new Error(`Access token response had status ${response.status}.`);
    }
  } catch (err) {
    res.status(503).json({ error: err.message });
    return;
  }

  const session = await encodeState(
    accessToken,
    encryption_password,
    Date.now() + TOKEN_VALIDITY_PERIOD,
  );
  returnUrl.searchParams.set(&apos;giscus&apos;, session);

  res.redirect(302, returnUrl.href);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;giscus App 会解析和验证 &lt;code&gt;state&lt;/code&gt; 参数，然后构造一个服务器对服务器的 POST 请求向 GitHub 请求访问令牌，请求体包含 &lt;code&gt;client_id&lt;/code&gt;、&lt;code&gt;client_secret&lt;/code&gt;、&lt;code&gt;code&lt;/code&gt; 和 &lt;code&gt;state&lt;/code&gt; 参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五步：GitHub 返回访问令牌给 giscus App&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GitHub 验证 &lt;code&gt;client_secret&lt;/code&gt; 和 &lt;code&gt;code&lt;/code&gt; 后，返回 JSON 数据。giscus App 从响应中提取出 &lt;code&gt;access_token&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第六步：giscus App 创建加密 Session 并重定向回博客站点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;giscus App 没有像传统 Web 应用那样用这个 token 直接为用户创建会话 Cookie，而是将 token 安全地“传回”给博客站点上的 giscus 前端组件。&lt;/p&gt;
&lt;p&gt;它将得到的访问令牌加密成一个名为 &lt;code&gt;session&lt;/code&gt; 的字符串，并将解密得到的 &lt;code&gt;appReturnUrl&lt;/code&gt;（即博客文章页面的地址）在其 URL 参数后附加 &lt;code&gt;?giscus=&amp;lt;加密的session字符串&amp;gt;&lt;/code&gt;，然后向访客的浏览器返回一个 302 重定向指令。&lt;/p&gt;
&lt;p&gt;最终重定向 URL，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET http://localhost:4321/posts/astro-firefly-blog-deploy-part4/?
  giscus=84c57886f2d88be979c1674dQODpjiFTL2GxjTO2QRcnHEHvvyshy%2Bxf5iI6Re5SlXkrbo6o0MkqMMLyr8X8fL3zxlSvW3IFtr5OTbmJ%2FQN%2Fw%2BDmch0KKPwK4bViYUoeI5gtD1P5WNI8d5Xg08s%3D#comments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访客的浏览器此刻被带回了最初的博客文章页面，并且 URL 中携带了一个神秘的 &lt;code&gt;giscus&lt;/code&gt; 参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第七步：静态博客网页处理 giscus 回调参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当浏览器被重定向回后，嵌入在页面中的 giscus 客户端脚本（&lt;a href=&quot;https://github.com/giscus/giscus/blob/main/client.ts&quot;&gt;client.ts&lt;/a&gt;）开始工作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检测并存储 Session：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Set up session and clear the session param on load
const url = new URL(location.href);
let session = url.searchParams.get(&apos;giscus&apos;) || &apos;&apos;;
const savedSession = localStorage.getItem(GISCUS_SESSION_KEY);
url.searchParams.delete(&apos;giscus&apos;);
url.hash = &apos;&apos;;
const cleanedLocation = url.toString();

if (session) {
  localStorage.setItem(GISCUS_SESSION_KEY, JSON.stringify(session));
  history.replaceState(undefined, document.title, cleanedLocation);
} else if (savedSession) {
  try {
    session = JSON.parse(savedSession);
  } catch (e) {
    localStorage.removeItem(GISCUS_SESSION_KEY);
    console.warn(`${formatError(e?.message)} Session has been cleared.`);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加密的 &lt;code&gt;session&lt;/code&gt; 字符串被安全地存储在浏览器 &lt;code&gt;localStorage&lt;/code&gt; 中，URL 被清理干净。此时，前端持有了代表用户授权状态的凭证（加密的 session），但还无法直接使用它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第八步：giscus Widget Iframe 加载并传递 Session&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;giscus 客户端脚本开始构造并加载评论组件的 Iframe。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造 widget 参数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const attributes = script.dataset;
const params: Record&amp;lt;string, string&amp;gt; = {};

params.origin = cleanedLocation;
params.session = session as string;
params.theme = attributes.theme as string;
params.reactionsEnabled = attributes.reactionsEnabled || &apos;1&apos;;
params.emitMetadata = attributes.emitMetadata || &apos;0&apos;;
params.inputPosition = attributes.inputPosition || &apos;bottom&apos;;
params.repo = attributes.repo as string;
params.repoId = attributes.repoId as string;
params.category = attributes.category || &apos;&apos;;
params.categoryId = attributes.categoryId as string;
params.strict = attributes.strict || &apos;0&apos;;
params.description = getMetaContent(&apos;description&apos;, true);
params.backLink = getMetaContent(&apos;giscus:backlink&apos;) || cleanedLocation;

switch (attributes.mapping) {
  case &apos;url&apos;:
    params.term = cleanedLocation;
    break;
  case &apos;title&apos;:
    params.term = document.title;
    break;
  case &apos;og:title&apos;:
    params.term = getMetaContent(&apos;title&apos;, true);
    break;
  case &apos;specific&apos;:
    params.term = attributes.term as string;
    break;
  case &apos;number&apos;:
    params.number = attributes.term as string;
    break;
  case &apos;pathname&apos;:
  default:
    params.term =
      location.pathname.length &amp;lt; 2
        ? &apos;index&apos;
        : location.pathname.substring(1).replace(/\.\w+$/, &apos;&apos;);
    break;
}

// Check anchor of the existing container and append it to origin URL
const existingContainer = document.querySelector(&apos;.giscus&apos;);
const id = existingContainer &amp;amp;&amp;amp; existingContainer.id;
if (id) {
  params.origin = `${cleanedLocation}#${id}`;
}

// Set up iframe src and loading attribute
const locale = attributes.lang ? `/${attributes.lang}` : &apos;&apos;;
const src = `${giscusOrigin}${locale}/widget?${new URLSearchParams(params)}`;
const loading = attributes.loading === &apos;lazy&apos; ? &apos;lazy&apos; : undefined;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终生成的 Iframe URL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://giscus.app/zh-CN/widget?
  origin=http://localhost:4321/posts/astro-firefly-blog-deploy-part4/#comments&amp;amp;session=9217fcd7459b93e3e27c1e25V7QDChvHmxQVKInMCD9q7LmScNiJfdGOBFZalw4jOEKaUeEPkQV2pgryenES7rtkfn4u3n9deNbmgvgCPXpMb4n5QsySzFbOGounl/MNrSEd6gTuX5nGeK8j0Vw=&amp;amp;
  repo=xpfxzxc/xpfxzxc.github.io&amp;amp;
  repoId=R_kgDOQIvG_Q&amp;amp;
  category=Announcements&amp;amp;
  categoryId=DIC_kwDOQIvG_c4Cxnv0&amp;amp;
  term=基于 Firefly 主题的 Astro 博客部署（四）：giscus 评论系统配置 - 未来之蓝 | xpfxzxc 的个人博客&amp;amp;
  number=&amp;amp;
  strict=0&amp;amp;
  reactionsEnabled=1&amp;amp;
  emitMetadata=1&amp;amp;
  inputPosition=top&amp;amp;
  theme=light&amp;amp;
  description=记录了自己在基于 Firefly 主题的 Astro 博客上配置启用 giscus 评论系统，并简单总结了该评论系统的部分流程。&amp;amp;
  backLink=http://localhost:4321/posts/astro-firefly-blog-deploy-part4/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建并加载 Iframe：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Set up iframe element
const iframeElement = document.createElement(&apos;iframe&apos;);
const iframeAttributes = {
  class: &apos;giscus-frame giscus-frame--loading&apos;,
  title: &apos;Comments&apos;,
  scrolling: &apos;no&apos;,
  allow: &apos;clipboard-write&apos;,
  src,
  loading,
};
Object.entries(iframeAttributes).forEach(
  ([key, value]) =&amp;gt; value &amp;amp;&amp;amp; iframeElement.setAttribute(key, value),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;iframe 开始加载，向 &lt;code&gt;giscus.app/widget&lt;/code&gt; 发起请求，并带上了所有配置参数和加密的 &lt;code&gt;session&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第九步：giscus Widget 服务端解密 Session 获取 Token&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当 iframe 加载 &lt;code&gt;https://giscus.app/widget?...&lt;/code&gt; 时，由 &lt;strong&gt;Next.js&lt;/strong&gt; 的 &lt;code&gt;getServerSideProps&lt;/code&gt; 服务端处理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务端接收参数：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const session = (query.session as string) || &apos;&apos;;
const repo = (query.repo as string) || &apos;&apos;;
// ... 接收其他参数
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;解密 Session 获取 Access Token：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const { encryption_password } = env;
const token = await decodeState(session, encryption_password)
  .catch(() =&amp;gt; getAppAccessToken(repo))
  .catch(() =&amp;gt; &apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;服务端渲染 Widget：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;使用获取到的 &lt;code&gt;token&lt;/code&gt;，服务端可以调用 GitHub API 获取评论数据、用户信息等&lt;/li&gt;
&lt;li&gt;将数据作为 props 传递给 React 组件，渲染出包含评论列表和评论框的完整 UI&lt;/li&gt;
&lt;li&gt;服务端会设置重要的 CSP 头，确保 iframe 只能被授权的站点嵌入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第十步：前端 Token 的安全使用与消息通信&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Token 不暴露给父页面&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解密出的真实 Access Token 仅存在于 giscus App 和它返回的 Widget 页面上下文中。Token 永远不会发送回博客站点的父页面脚本，从而避免了潜在的前端安全风险。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于 postMessage 的通信&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当访客在 Iframe 的评论框中发表评论时，&amp;lt;u&amp;gt;iframe 内部的 giscus 代码会直接使用它拥有的 Token 去调用 GitHub API*&amp;lt;/u&amp;gt;。&lt;/p&gt;
&lt;p&gt;iframe 与父页面之间通过 &lt;code&gt;postMessage&lt;/code&gt; 进行安全的跨域通信，仅传递 UI 状态，绝不传递 Access Token。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Listen to messages
window.addEventListener(&apos;message&apos;, (event) =&amp;gt; {
  if (event.origin !== giscusOrigin) return;

  const { data } = event;
  if (!(typeof data === &apos;object&apos; &amp;amp;&amp;amp; data.giscus)) return;

  if (data.giscus.resizeHeight) {
    iframeElement.style.height = `${data.giscus.resizeHeight}px`;
  }

  if (data.giscus.signOut) {
    localStorage.removeItem(GISCUS_SESSION_KEY);
    console.log(`[giscus] User has logged out. Session has been cleared.`);
    signOut();
    return;
  }
  // ...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第十一步：用户状态维持与登出&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只要 localStorage 中的加密 &lt;code&gt;session&lt;/code&gt; 不被清除，访客下次访问博客站点时，giscus 客户端会自动从 localStorage 读取 &lt;code&gt;session&lt;/code&gt; 并传递给 Widget，让访客保持登录状态。&lt;/p&gt;
&lt;p&gt;当在 Widget 中点击登出时，iframe 会通过 &lt;code&gt;postMessage&lt;/code&gt; 发送一个 &lt;code&gt;signOut&lt;/code&gt; 指令。博客文章页面的 giscus 客户端收到后，会重置 iframe 的 &lt;code&gt;src&lt;/code&gt;（不带 session），强制重新加载为未登录状态，同时清理本地存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function signOut() {
  delete params.session;
  const src = `${giscusOrigin}${locale}/widget?${new URLSearchParams(params)}`;
  iframeElement.src = src; // Force reload
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>基于 Firefly 主题的 Astro 博客部署（三）：站点配置修改</title><link>https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part3/</link><guid isPermaLink="true">https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part3/</guid><description>记录了自己在基于 Firefly 主题的 Astro 博客上修改个人名称、头像、介绍、公告、背景壁纸等配置的操作。</description><pubDate>Wed, 19 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在成功地在 &lt;strong&gt;GitHub Pages&lt;/strong&gt; 上部署了博客，并紧接着在博客上发布文章后，看着自己撰写的文章被编排、美化成好看的页面显示在博客网站上，我的内心里瞬间有了以后在博客上撰写文章的动力。&lt;/p&gt;
&lt;p&gt;目前，我的博客网站上包括个人名称、头像、介绍、公告、背景壁纸等在内的各种信息，仍是原 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot;&gt;&lt;strong&gt;Firefly&lt;/strong&gt;&lt;/a&gt; 主题作者预设的默认内容。我计划接下来逐步替换这些信息。毕竟，GitHub Pages 上的内容能够被搜索引擎收录的，作为博主，我当然不希望自己写的文章被他人阅读时，被误认为是他人所写；同时，也不希望博客中的其他信息与其他人雷同。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/config&lt;/code&gt; 目录下存放着博客的相关配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
├── config/
│   ├── index.ts              # 配置索引文件
│   ├── siteConfig.ts         # 站点基础配置
│   ├── profileConfig.ts      # 用户资料配置
│   ├── commentConfig.ts      # 评论系统配置
│   ├── announcementConfig.ts # 公告配置
│   ├── licenseConfig.ts      # 许可证配置
│   ├── footerConfig.ts       # 页脚配置
│   ├── FooterConfig.html     # 页脚HTML内容
│   ├── expressiveCodeConfig.ts # 代码高亮配置
│   ├── sakuraConfig.ts       # 樱花特效配置
│   ├── fontConfig.ts         # 字体配置
│   ├── sidebarConfig.ts      # 侧边栏布局配置
│   ├── navBarConfig.ts       # 导航栏配置
│   ├── musicConfig.ts        # 音乐播放器配置
│   ├── pioConfig.ts          # 看板娘配置
│   ├── adConfig.ts           # 广告配置
│   ├── friendsConfig.ts      # 友链配置
│   └── sponsorConfig.ts      # 赞助配置
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;元数据配置&lt;/h2&gt;
&lt;p&gt;站点元数据配置，主要包括标题、描述、关键词和 &lt;strong&gt;Favicon&lt;/strong&gt;。这些配置信息对于 &lt;strong&gt;SEO&lt;/strong&gt; 至关重要，通常会被注入到网站的 HTML 头部（&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 区域），供浏览器和搜索引擎读取。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;siteConfig.ts&lt;/code&gt; 文件里存放着这些配置信息。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;title&lt;/code&gt; 和 &lt;code&gt;subtitle&lt;/code&gt; 字段，最终会被如何使用，&lt;code&gt;src/layouts/Layout.astro&lt;/code&gt; 文件里有相关代码段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let pageTitle: string;
if (title) {
	pageTitle = `${title} - ${siteConfig.title}`;
} else {
	pageTitle = siteConfig.subtitle
		? `${siteConfig.title} - ${siteConfig.subtitle}`
		: siteConfig.title;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码根据是否传入 &lt;code&gt;title&lt;/code&gt; 参数来决定页面标题的格式，这样可以确保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文章页面显示：&lt;code&gt;文章标题 - &amp;lt;title&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;首页显示：&lt;code&gt;&amp;lt;title&amp;gt; - &amp;lt;subtitle&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其他页面根据传入的 &lt;code&gt;title&lt;/code&gt; prop 显示对应标题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了有利于 SEO，对于标题、描述和关键词，我将 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件内容修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const siteConfig: SiteConfig = {
-  title: &quot;Firefly&quot;,
-  subtitle: &quot;Demo site&quot;,
-  description:
-    &quot;Firefly 是一款基于 Astro 框架开发的清新美观且现代化个人博客主题，专为技术爱好者和内容创作者设计。该主题融合了现代 Web 技术栈，提供了丰富的功能模块和高度可定制的界面，让您能够轻松打造出专业且美观的个人博客网站。&quot;,
+  title: &quot;未来之蓝 | xpfxzxc 的个人博客&quot;,
+  subtitle: &quot;个人学习和日常零散想法的记录空间，内容随性且不定期更新&quot;,
+  description: &quot;未来之蓝是 xpfxzxc 的个人博客，记录个人学习和日常零散想法。心怀蔚蓝愿景，脚踏实地前行。每一篇记录都是通向理想未来的坚实脚印。&quot;,
  keywords: [
-    &quot;Firefly&quot;,
-    &quot;Fuwari&quot;,
-    &quot;Astro&quot;,
-    &quot;ACGN&quot;,
-    &quot;博客&quot;,
-    &quot;技术博客&quot;,
-    &quot;静态博客&quot;,
+    &quot;xpfxzxc&quot;, &quot;未来之蓝&quot;, &quot;个人博客&quot;,
+    &quot;个人成长&quot;, &quot;技术分享&quot;, &quot;生活随想&quot;,
+    &quot;编程学习&quot;, &quot;ACGN&quot;, &quot;动漫&quot;, &quot;二次元&quot;, &quot;游戏&quot;,
+    &quot;非专业博客&quot;, &quot;学习记录&quot;, &quot;读书笔记&quot;
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Favicon&lt;/strong&gt; 是 favorites icon 的缩写，也被称为网页图标，它是与网站相关联的图标，通常显示在浏览器的地址栏、书签栏或标签页上。通常而言，Favicon 与网站 Logo 在设计上是相同的，仅尺寸不同。它也可能是 Logo 中的首字或首字母，或是某个标志性图案的提炼。&lt;/p&gt;
&lt;p&gt;我打算先完成 Logo 的设计（具体步骤后述），随后使用 &lt;a href=&quot;https://imagemagick.org&quot;&gt;&lt;strong&gt;ImageMagick&lt;/strong&gt;&lt;/a&gt; 将其转换为 ICO 格式（&lt;code&gt;.ico&lt;/code&gt;），目标尺寸为 32×32 像素（假设 Logo 原始尺寸已为 1:1 比例）:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick logo.png -resize 32x32 favicon.ico
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换了 Favicon 后，维持 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件内相关配置代码段不动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
favicon: [
  // 留空以使用默认 favicon
  {
    src: &quot;/assets/images/favicon.ico&quot;, // 图标文件路径
    theme: &quot;light&quot;, // 可选，指定主题 &apos;light&apos; | &apos;dark&apos;
    sizes: &quot;32x32&quot;, // 可选，图标大小
  },
],
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果将 &lt;code&gt;favicon&lt;/code&gt; 字段设置为空数组，将使用 &lt;code&gt;public/favicon&lt;/code&gt; 目录里的图标。&lt;/p&gt;
&lt;h2&gt;站点 Logo 和名称&lt;/h2&gt;
&lt;p&gt;站点 Logo 和名称位于顶部导航栏的左部分。其配置不在 &lt;code&gt;navBarConfig.ts&lt;/code&gt; 文件中，而在 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件中的一部分。&lt;/p&gt;
&lt;p&gt;Logo 与相对通用的头像或背景壁纸不同，它需要具备独特的辨识度，以避免与其他站点雷同，从而体现博客的个性。因此，若懂设计，强烈推荐亲自设计。若不懂，也有以下解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI 智能生成（抽卡）：
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.canva.com&quot;&gt;&lt;strong&gt;Canva&lt;/strong&gt;&lt;/a&gt;：全球最流行的在线设计工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://novelai.net&quot;&gt;&lt;strong&gt;NovelAI&lt;/strong&gt;&lt;/a&gt;：专注于二次元图像生成和故事创作&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stabledifffusion.com&quot;&gt;&lt;strong&gt;Stable Diffusion&lt;/strong&gt;&lt;/a&gt;：免费、开源、控制力强，拥有大量各种的模型，可本地部署或使用在线平台&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.midjourney.com&quot;&gt;&lt;strong&gt;Midjourney&lt;/strong&gt;&lt;/a&gt;：收费、闭源，艺术感和质感之王&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.liblib.art&quot;&gt;&lt;strong&gt;LibibAI&lt;/strong&gt;&lt;/a&gt;：国内领先的 AI 创作平台，提供了原汁原味的 &lt;strong&gt;webUI&lt;/strong&gt;、&lt;strong&gt;comfyUI&lt;/strong&gt; 等在线 AI 绘图工具免费试用，可在线进行模型训练、使用各种模型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;打造纯文字 Logo：挑选一款特色字体，文字使用中英文组合，进行细节修饰、微调&lt;/li&gt;
&lt;li&gt;交给专业人士：
&lt;ul&gt;
&lt;li&gt;淘宝/咸鱼：搜索“Logo设计”，有许多工作室接单，价格非常亲民&lt;/li&gt;
&lt;li&gt;专业设计平台：&lt;a href=&quot;https://creativesku.com&quot;&gt;&lt;strong&gt;特赞&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://www.zcool.com.cn/&quot;&gt;&lt;strong&gt;站酷&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;社交媒体：在微博、小红书等平台上找独立设计师约稿&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Firefly 主题风格简洁美观，并且与二次元元素非常契合，这从主题演示站点的出色效果可见一斑。这让我决定采用二次元风格的头像和背景壁纸。自然地，与之配套的 Logo 和 Favicon 也应选择统一的二次元风格。&lt;/p&gt;
&lt;p&gt;我打算先在 NovelAI 平台上输入 Prompt 来进行“抽卡”，看能不能抽出看得过去的 Logo。NovelAI 平台的新账号拥有 30 张图片的免费生成额度。&lt;/p&gt;
&lt;p&gt;我通过 AI 来辅助生成 Prompt：首先，我将网站的标题、副标题、描述、关键词，还有之后要使用的头像和背景壁纸的相关信息输入到 AI；然后，将其生成的多个 Prompt 逐一复制到 NovelAI 里的 &lt;code&gt;Prompt&lt;/code&gt; 输入框里，并执行图片生成。在生成图片之前，可以设置其尺寸大小，例如：1:1 比例的 1024×1024 尺寸。根据生成的情况，可能要尝试好几种 Prompt，也可能需对同一个 Prompt 多次执行图片生成。最后，从生成的多张图片中挑选出几张看得过去的图片。&lt;/p&gt;
&lt;p&gt;我最终使用的 Logo，是根据以下 Prompt 生成的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;masterpiece, minimalist logo, an open book from which emerges a constellation of three stars flying upwards, symbolizing ideas reaching for the future. Solid color design in hue 230, white background, clean vector graphic, no gradient --niji 6 --style raw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该 Prompt 生成的图片通常为白底。若直接用作 Logo，视觉效果不佳，因此下一步需移除背景，将其变为透明。可以选用 &lt;a href=&quot;https://www.remove.bg&quot;&gt;&lt;strong&gt;removebg&lt;/strong&gt;&lt;/a&gt; 等在线工具或其他软件来移除背景。&lt;/p&gt;
&lt;p&gt;目前得到的 Logo 图片格式是 PNG。作为优化流程的一环，Logo 应被转换为 WEBP 这类现代图片格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick logo.png -quality 85 logo.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给博客站点取了个名称后，我将 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件内容修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
// 导航栏Logo
// navbarLogo 支持三种类型：Astro图标库，本地图片，网络图片
// { type: &quot;icon&quot;, value: &quot;material-symbols:home-pin-outline&quot; }
// { type: &quot;image&quot;, value: &quot;/assets/images/logo.webp&quot;, alt: &quot;Firefly Logo&quot; }
// { type: &quot;image&quot;, value: &quot;https://example.com/logo.png&quot;, alt: &quot;Firefly Logo&quot; }
navbarLogo: {
  type: &quot;image&quot;,
-    value: &quot;/assets/images/LiuYingPure3.svg&quot;,
+    value: &quot;/assets/images/logo.webp&quot;,
-    alt: &quot;🍀&quot;,
+    alt: &quot;未来之蓝博客 Logo - 一本翻开的书与飞向上方的星辰，象征知识、记录与通向未来的愿景&quot;,
  },
-  navbarTitle: &quot;Firefly&quot;, // 导航栏标题，可以设置为与 title 不同的值，如果不设置则使用 title
+  navbarTitle: &quot;未来之蓝&quot;, // 导航栏标题，可以设置为与 title 不同的值，如果不设置则使用 title
}
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;个人基本资料&lt;/h2&gt;
&lt;p&gt;个人基本资料位于页面的左侧栏。&lt;code&gt;profileConfig.ts&lt;/code&gt; 文件里存放着个人资料配置，例如修改个人头像、名称、介绍和社交栏设置。&lt;/p&gt;
&lt;p&gt;个人头像的获取渠道很多，选取时需与网站主题风格保持一致。个人名称可直接使用自己熟悉的网名。个人介绍若无灵感，可先参考其他博主的写法。在社交栏处，可以登记下自己常用的社交媒体链接。&lt;/p&gt;
&lt;p&gt;对于像头像这样较大或更大的图片，最好使用 &lt;strong&gt;WebP&lt;/strong&gt; 格式，WebP 的优势如下：&lt;/p&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;使用 ImageMagick 将 &lt;strong&gt;PNG&lt;/strong&gt; 或 &lt;strong&gt;JPG&lt;/strong&gt; 批量或单个转换为 WebP 格式很简单，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 有损压缩，质量 85（常用）
magick avatar.png -quality 85 avatar.webp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换了头像图片之后，我将 &lt;code&gt;profileConfig.ts&lt;/code&gt; 文件内容修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { ProfileConfig } from &quot;../types/config&quot;;

export const profileConfig: ProfileConfig = {
  avatar: &quot;/assets/images/avatar.webp&quot;,
-  name: &quot;Firefly&quot;,
-  bio: &quot;Hello, I&apos;m Firefly.&quot;,
+  name: &quot;xpfxzxc&quot;,
+  bio: &quot;不愿沉溺于无力感，正试着将碎片化的思考转化成可见的成长足迹。&quot;,
  links: [
    {
-      name: &quot;Bilibli&quot;,
-      icon: &quot;fa6-brands:bilibili&quot;,
-      url: &quot;https://space.bilibili.com/38932988&quot;,
+      name: &quot;Email&quot;,
+      icon: &quot;material-symbols:mail&quot;,
+      url: &quot;mailto:xpfxzxc@gmail.com&quot;,
    },
    {
      name: &quot;GitHub&quot;,
      icon: &quot;fa6-brands:github&quot;,
-      url: &quot;https://github.com/CuteLeaf&quot;,
+      url: &quot;https://github.com/xpfxzxc&quot;,
+    },
+    {
+      name: &quot;Bilibli&quot;,
+      icon: &quot;fa6-brands:bilibili&quot;,
+      url: &quot;https://space.bilibili.com/12533717&quot;,
+    },
+    {
+      name: &quot;Steam&quot;,
+      icon: &quot;fa6-brands:steam&quot;,
+      url: &quot;https://steamcommunity.com/profiles/76561198327614842&quot;,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;icon&lt;/code&gt; 字段，在网络上搜索 &lt;code&gt;fa6-brands online&lt;/code&gt;，即可查询到相关的图标搜索网站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fontawesome.com/search&quot;&gt;FontAwesome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://iconbuddy.com/fa6-brands&quot;&gt;Iconbuddy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;站点公告&lt;/h2&gt;
&lt;p&gt;公告同样位于页面的左侧栏。&lt;code&gt;announcementConfig.ts&lt;/code&gt; 文件里存放着公告配置，但卡片上的 &lt;code&gt;公告&lt;/code&gt; 字样可以自定义为其他标题，因此严格来说该区域的内容不一定是“公告”？&lt;/p&gt;
&lt;p&gt;我将文件内容修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { AnnouncementConfig } from &quot;../types/config&quot;;

export const announcementConfig: AnnouncementConfig = {
-  title: &quot;公告&quot;, // 公告标题
-  content: &quot;欢迎来到我的博客！这是一则示例公告。&quot;, // 公告内容
-  closable: true, // 允许用户关闭公告
+  title: &quot;站点说明&quot;, // 公告标题
+  content: &quot;个人学习和日常零散想法的记录空间，内容随性且不定期更新。&quot;, // 公告内容
+  closable: false, // 禁止用户关闭公告
  link: {
-    enable: true, // 启用链接
+    enable: false, // 关闭链接
    text: &quot;了解更多&quot;, // 链接文本
    url: &quot;/about/&quot;, // 链接 URL
    external: false, // 内部链接
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;导航栏&lt;/h2&gt;
&lt;p&gt;导航栏位于页面的顶部中间部分。&lt;code&gt;navBarConfig.ts&lt;/code&gt; 文件里存放着导航栏配置，它是一个链接对象数组。每个链接对象，要么是 &lt;code&gt;LinkPreset&lt;/code&gt; 链接预设枚举，要么是自定义链接对象。当自定义链接对象时，使用 &lt;code&gt;children&lt;/code&gt; 字段可以生成多级菜单。&lt;/p&gt;
&lt;p&gt;我将文件内容修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { NavBarConfig, NavBarLink } from &quot;../types/config&quot;;
import { LinkPreset } from &quot;../types/config&quot;;
import { siteConfig } from &quot;./siteConfig&quot;;

// 根据页面开关动态生成导航栏配置
const getDynamicNavBarConfig = (): NavBarConfig =&amp;gt; {
  const links: (NavBarLink | LinkPreset)[] = [
    LinkPreset.Home,
    LinkPreset.Archive,
  ];

  // 支持自定义导航栏链接,并且支持多级菜单
  links.push({
    name: &quot;链接&quot;,
    url: &quot;/links/&quot;,
    icon: &quot;material-symbols:link&quot;,
    children: [
+      {
+        name: &quot;Email&quot;,
+        url: &quot;mailto:xpfxzxc@gmail.com&quot;,
+        external: true,
+        icon: &quot;material-symbols:mail&quot;,
+      },
      {
        name: &quot;GitHub&quot;,
-        url: &quot;https://github.com/CuteLeaf/Firefly&quot;,
+        url: &quot;https://github.com/xpfxzxc&quot;,
        external: true,
        icon: &quot;fa6-brands:github&quot;,
      },
      {
        name: &quot;Bilibili&quot;,
-        url: &quot;https://space.bilibili.com/38932988&quot;,
+        url: &quot;https://space.bilibili.com/12533717&quot;,
        external: true,
        icon: &quot;fa6-brands:bilibili&quot;,
      },
+      {
+        name: &quot;Steam&quot;,
+        url: &quot;https://steamcommunity.com/profiles/76561198327614842&quot;,
+        external: true,
+        icon: &quot;fa6-brands:steam&quot;,
+      },
    ],
  });

  links.push(LinkPreset.Friends);

  // 根据配置决定是否添加留言板页面
  if (siteConfig.pages.guestbook) {
    links.push(LinkPreset.Guestbook);
  }

  links.push({
    name: &quot;关于&quot;,
    url: &quot;/content/&quot;,
    icon: &quot;material-symbols:info&quot;,
    children: [
      ...(siteConfig.pages.anime ? [LinkPreset.Anime] : []), // 根据配置决定是否添加追番页面
      ...(siteConfig.pages.sponsor ? [LinkPreset.Sponsor] : []), // 根据配置决定是否添加赞助页面
      LinkPreset.About,
    ],
  });
  return { links };
};

export const navBarConfig: NavBarConfig = getDynamicNavBarConfig();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 &lt;code&gt;icon&lt;/code&gt; 字段里用到的 &lt;strong&gt;Material Symbols&lt;/strong&gt;，更多图标可在 &lt;a href=&quot;https://fonts.google.com/icons&quot;&gt;Material Symbols and Icons&lt;/a&gt; 网站中搜索。&lt;/p&gt;
&lt;h2&gt;背景壁纸&lt;/h2&gt;
&lt;p&gt;虽然 &lt;a href=&quot;https://unsplash.com&quot;&gt;&lt;strong&gt;Unsplash&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://www.pexels.com&quot;&gt;&lt;strong&gt;Pexels&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://pixabay.com&quot;&gt;&lt;strong&gt;Pixabay&lt;/strong&gt;&lt;/a&gt; 等网站提供海量高质量图片，但是我不喜欢这些网站上的图片风格，比如：自然、生活、简约、插画、抽象、纹理、摄影、渐变、写实等。我认为这些图片倒是比较适合作为商业册子上的图案或者电子设备上的背景壁纸。其中，大部分图片看着令我感到压抑感；小部分图片单独看着还可以，但作为网页背景壁纸时不太适配 Firefly 主题现有元素风格。与其用这些背景壁纸，不如采用极简风格的色彩搭配或直接使用纯色背景。&lt;/p&gt;
&lt;p&gt;既然我之前选用的头像是二次元风格，而且 Firefly 主题演示站点用的背景壁纸看起来美观，令人感到舒适，那就使用 &lt;a href=&quot;https://www.pixiv.co.jp&quot;&gt;&lt;strong&gt;Pixiv&lt;/strong&gt;&lt;/a&gt; 或 &lt;a href=&quot;https://www.zerochan.net&quot;&gt;&lt;strong&gt;zerochan&lt;/strong&gt;&lt;/a&gt; 等网站上高质量插画来作为博客的背景壁纸。在选择的过程中，要注意避免版权问题，最好选择作者明确标明“可自由使用” 或 &lt;strong&gt;CC0&lt;/strong&gt;、&lt;strong&gt;CC BY&lt;/strong&gt; 授权的插画。&lt;/p&gt;
&lt;p&gt;此外，在网友自建的壁纸站里找高质量壁纸也是一种很好的选择。比如：&lt;a href=&quot;https://www.lovewind.de&quot;&gt;&lt;strong&gt;恋风画廊 - 高质量壁纸下载&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;针对不同设备的屏幕比例特性，一般来说：移动端优先采用竖屏背景壁纸，而非移动端（如 PC、平板）则统一采用横屏背景壁纸。&lt;/p&gt;
&lt;p&gt;为了优化浏览体验，建议将选定的背景壁纸转换为 WEBP 格式。可以替换掉 &lt;code&gt;public/assets/images&lt;/code&gt; 目录下的 &lt;code&gt;d1.webp&lt;/code&gt;（桌面端）和 &lt;code&gt;m1.webp&lt;/code&gt;（移动端）文件，也可另取文件名保存。注意，将背景壁纸本地化保存可有效避免因外部链接失效导致的显示问题。&lt;/p&gt;
&lt;p&gt;背景壁纸相关的配置在 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件里 &lt;code&gt;backgroundWallpaper&lt;/code&gt; 字段上，里面有非常多的选项可以配置。几乎所有的选项都已有注释，非常好理解，不过，&lt;code&gt;position&lt;/code&gt; 选项的理解需要额外学习一些 &lt;strong&gt;CSS&lt;/strong&gt; 知识点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
// 图片位置
// 支持所有CSS object-position值，如: &apos;top&apos;, &apos;center&apos;, &apos;bottom&apos;, &apos;left top&apos;, &apos;right bottom&apos;, &apos;25% 75%&apos;, &apos;10px 20px&apos;..
// 如果不知道怎么配置百分百之类的配置，推荐直接使用：&apos;center&apos;居中，&apos;top&apos;顶部居中，&apos;bottom&apos; 底部居中，&apos;left&apos;左侧居中，&apos;right&apos;右侧居中
position: &quot;0% 20%&quot;,
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据代码注释和源码，这里 &lt;code&gt;position&lt;/code&gt; 选项的值会直接运用到 CSS 的 &lt;code&gt;object-position&lt;/code&gt; 属性上。&lt;code&gt;object-position&lt;/code&gt; 属性的作用：想象成照片（可替换元素）在相框（内容框）里移动，具体来说，照片上的哪个点与相框上的哪个点对齐。&lt;/p&gt;
&lt;p&gt;CSS &lt;code&gt;object-fit&lt;/code&gt; 通常配合 &lt;code&gt;object-position&lt;/code&gt; 属性使用，它的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cover&lt;/code&gt;：照片按其宽高比进行缩放，直至完全覆盖相框，可能会裁剪一部分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contain&lt;/code&gt;：照片按其宽高比进行缩放，直到完整显示在相框内，可能会留有空白&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fill&lt;/code&gt;（默认）：照片会被拉伸或压缩，以填满整个相框，可能会变形&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在源码中，已经将 &lt;code&gt;object-fit&lt;/code&gt; 属性的值设置成 &lt;code&gt;cover&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;综上来看，配置文件中的 &lt;code&gt;position: 0 20%&lt;/code&gt; 的含义是：保持相框不动，让图片在垂直方向上向上移动，从而在容器中展示出图片更靠下的区域，就等于告诉浏览器：“不想看这张大图最顶上的一部分，请把画面往下挪一点再给我看。”这表明如果使用这个值的话，使用的背景壁纸顶部将被裁剪掉的一小部分不要有“太重要”的内容。&lt;/p&gt;
&lt;p&gt;在“横幅壁纸”模式下，主页横幅区域设有标题与副标题文字，其中副标题会从预设的若干组文字中动态轮换，并呈现逐字键入与删除的动态效果。文字内容需要兼顾节奏与韵律、意境与画面、情感共鸣、系列感与统一。&lt;/p&gt;
&lt;p&gt;在该模式下，当前可能存在两个显示协调性问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在主页面中，横幅的主标题和副标题可能会遮挡背景壁纸的关键部分（如角色脸部），影像画面整体协调，此时可能需要调整配置文件中的 &lt;code&gt;position&lt;/code&gt; 字段。&lt;/li&gt;
&lt;li&gt;其他页面的背景壁纸位置相较于主页会略微上移，下方卡片区域也向上移动，导致背景壁纸被遮挡的区域更大，画面看起来不协调。因此，同样可能需要通过调整 &lt;code&gt;position&lt;/code&gt; 字段进行修正。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不过，即使调整好了 &lt;code&gt;position&lt;/code&gt; 字段，在其他视口尺寸的情况下，同样可能出现遮挡问题。要彻底规避遮挡问题，要么投入更多开发量引入响应式定位机制，要么改用“全屏壁纸”或“纯色背景”模式，甚至直接更换背景壁纸。&lt;/p&gt;
&lt;p&gt;最终，我将背景壁纸相关配置改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  backgroundWallpaper: {
    // 壁纸模式：&quot;banner&quot; 横幅壁纸，&quot;overlay&quot; 全屏壁纸，&quot;none&quot; 纯色背景无壁纸
    mode: &quot;banner&quot;,
    // 是否允许用户通过导航栏切换壁纸模式，设为false可提升性能（只渲染当前模式）
    switchable: true,

    // 背景图片配置
    src: {
      // 桌面背景图片
      desktop: &quot;/assets/images/d1.webp&quot;,
      // 移动背景图片
      mobile: &quot;/assets/images/m1.webp&quot;,
    },

    // Banner模式特有配置
    banner: {
      // 图片位置
      // 支持所有CSS object-position值，如: &apos;top&apos;, &apos;center&apos;, &apos;bottom&apos;, &apos;left top&apos;, &apos;right bottom&apos;, &apos;25% 75%&apos;, &apos;10px 20px&apos;..
      // 如果不知道怎么配置百分百之类的配置，推荐直接使用：&apos;center&apos;居中，&apos;top&apos;顶部居中，&apos;bottom&apos; 底部居中，&apos;left&apos;左侧居中，&apos;right&apos;右侧居中
-     position: &quot;0% 20%&quot;,
+     position: &quot;center 9%&quot;,
      
      homeText: {
        // 主页显示自定义文本（全局开关）
        enable: true,
        // 主页横幅主标题
-       title: &quot;Lovely firefly!&quot;,
+       title: &quot;Future Archives!&quot;,
        // 主页横幅副标题
        subtitle: [
-         &quot;In Reddened Chrysalis, I Once Rest&quot;,
-         &quot;From Shattered Sky, I Free Fall&quot;,
-         &quot;Amidst Silenced Stars, I Deep Sleep&quot;,
-         &quot;Upon Lighted Fyrefly, I Soon Gaze&quot;,
-         &quot;From Undreamt Night, I Thence Shine&quot;,
-         &quot;In Finalized Morrow, I Full Bloom&quot;,
+         &quot;In Reddened Chrysalis, I Once Rest&quot;,
+         &quot;From Shattered Sky, I Free Fall&quot;,
+         &quot;Amidst Silenced Stars, I Deep Sleep&quot;,
+         &quot;Upon Lighted Fyrefly, I Soon Gaze&quot;,
+         &quot;From Undreamt Night, I Thence Shine&quot;,
+         &quot;In Finalized Morrow, I Full Bloom&quot;,
        ],
        typewriter: {
          enable: true, // 启用副标题打字机效果
          speed: 100, // 打字速度（毫秒）
          deleteSpeed: 50, // 删除速度（毫秒）
          pauseTime: 2000, // 完全显示后的暂停时间（毫秒）
        },
      },
      credit: {
        enable: {
          desktop: true, // 桌面端显示横幅图片来源文本
-         mobile: false, // 移动端显示横幅图片来源文本
+         mobile: true, // 移动端显示横幅图片来源文本
        },
        text: {
-         desktop: &quot;Pixiv - 晚晚喵&quot;, // 桌面端要显示的来源文本
-         mobile: &quot;Mobile Credit&quot;, // 移动端要显示的来源文本
+         desktop: &quot;Pixiv - 鈴木シロリ&quot;, // 桌面端要显示的来源文本
+         mobile: &quot;Pixiv - Orivayne&quot;, // 移动端要显示的来源文本
        },
        url: {
-         desktop: &quot;https://www.pixiv.net/artworks/135490046&quot;, // 桌面端原始艺术品或艺术家页面的 URL 链接
-         mobile: &quot;&quot;, // 移动端原始艺术品或艺术家页面的 URL 链接
+         desktop: &quot;https://www.pixiv.net/artworks/108043909&quot;, // 桌面端原始艺术品或艺术家页面的 URL 链接
+         mobile: &quot;https://www.pixiv.net/artworks/130379063&quot;, // 移动端原始艺术品或艺术家页面的 URL 链接
        },
      },
      navbar: {
        transparentMode: &quot;semifull&quot;, // 导航栏透明模式：&quot;semi&quot; 半透明加圆角，&quot;full&quot; 完全透明，&quot;semifull&quot; 动态透明
      },
      // 波浪动画效果配置，开启可能会影响页面性能，请根据实际情况开启
      waves: {
        enable: {
          desktop: true, // 桌面端启用波浪动画效果
          mobile: true, // 移动端启用波浪动画效果
        },
        performance: {
          quality: &quot;high&quot;,
          hardwareAcceleration: true, // 是否启用硬件加速
        },
        // 性能优化说明：
        // quality: &quot;high&quot; - 最佳视觉效果，但GPU占用较高，适合高性能设备
        // quality: &quot;medium&quot; - 平衡性能和质量，适合中等性能设备
        // quality: &quot;low&quot; - 最低GPU占用，动画更简单，适合低性能设备
        // hardwareAcceleration: true - 启用GPU加速，提升性能但增加GPU占用
        // hardwareAcceleration: false - 禁用GPU加速，降低GPU占用但可能影响性能
      },
    },

    // 全屏透明覆盖模式特有配置
    overlay: {
      zIndex: -1, // 层级，确保壁纸在背景层
      opacity: 0.8, // 壁纸透明度
-     blur: 1, // 背景模糊程度
+     blur: 0.5, // 背景模糊程度
    },
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;主题色&lt;/h2&gt;
&lt;p&gt;更换好 Logo、Favicon、头像图片和背景壁纸后，下一步是选择合适的主题色，以确保网站各元素的色彩整体和谐统一。&lt;/p&gt;
&lt;p&gt;当前的主题色系统基于 &lt;strong&gt;OKLCH&lt;/strong&gt; 色彩空间构建。无论是通过配置文件还是界面调节器修改主题色，最终都会作用于 CSS 变量 &lt;code&gt;--hue&lt;/code&gt;。系统中主题相关的颜色，都是基于固定的&lt;strong&gt;亮度&lt;/strong&gt;（&lt;strong&gt;Lightness&lt;/strong&gt;）与&lt;strong&gt;色度&lt;/strong&gt;（&lt;strong&gt;Chroma&lt;/strong&gt;）值，结合可变的&lt;strong&gt;色相&lt;/strong&gt;（&lt;strong&gt;Hue&lt;/strong&gt;）值动态计算得出的。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* ===== 主题相关变量 ===== */
:root {
  --primary: oklch(0.70 0.14 var(--hue));
  --page-bg: oklch(0.95 0.01 var(--hue));
  --card-bg: white;
  --card-bg-transparent: rgba(255, 255, 255, 0.8);
  
  --btn-content: oklch(0.55 0.12 var(--hue));
  --btn-regular-bg: oklch(0.95 0.025 var(--hue));
  --btn-regular-bg-hover: oklch(0.9 0.05 var(--hue));
  --btn-regular-bg-active: oklch(0.85 0.08 var(--hue));
  /* ... */
}
/* ... */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，主题色可选的范围非常有限。&lt;/p&gt;
&lt;p&gt;我将 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件里的主题色配置为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;themeColor: {
-  hue: 165, // 主题色的默认色相，范围从 0 到 360。例如：红色：0，青色：200，蓝绿色：250，粉色：345
-  fixed: false, // 对访问者隐藏主题色选择器
+  hue: 230, // 主题色的默认色相，范围从 0 到 360。例如：红色：0，青色：200，蓝绿色：250，粉色：345
+  fixed: true, // 对访问者隐藏主题色选择器
  defaultMode: &quot;system&quot;, // 默认模式：&quot;light&quot; 亮色，&quot;dark&quot; 暗色，&quot;system&quot; 跟随系统
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;字体&lt;/h2&gt;
&lt;p&gt;基于目前的默认字体配置，虽说字体在我的电脑上显示得很美观，但是在其他电脑上就可能显示得不一样。对此，我有个需求：不管在什么设备上打开博客站点，页面上都要使用同样的字体。&lt;/p&gt;
&lt;p&gt;由于我不太了解字体方面，所以在配置前查阅了相关资料，并阅读了一些源码。&lt;/p&gt;
&lt;p&gt;字体的相关配置在 &lt;code&gt;fontConfig.ts&lt;/code&gt; 文件里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 字体配置
export const fontConfig = {
  enable: true, // 启用自定义字体功能
  preload: true, // 预加载字体文件以提高性能
  selected: [&quot;system&quot;], // 当前选择的字体，支持多个字体组合
  fonts: {
    // 系统字体
    system: {
      id: &quot;system&quot;,
      name: &quot;系统字体&quot;,
      src: &quot;&quot;, // 系统字体无需 src
      family:
        &quot;system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif&quot;,
    },
    // Google Fonts - Zen Maru Gothic
    &quot;zen-maru-gothic&quot;: {
      id: &quot;zen-maru-gothic&quot;,
      name: &quot;Zen Maru Gothic&quot;,
      src: &quot;https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@300;400;500;700;900&amp;amp;display=swap&quot;,
      family: &quot;Zen Maru Gothic&quot;,
      display: &quot;swap&quot; as const,
    },
    // Google Fonts - Inter
    inter: {
      id: &quot;inter&quot;,
      name: &quot;Inter&quot;,
      src: &quot;https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;amp;display=swap&quot;,
      family: &quot;Inter&quot;,
      display: &quot;swap&quot; as const,
    },
    // 小米字体 - MiSans Normal
    &quot;misans-normal&quot;: {
      id: &quot;misans-normal&quot;,
      name: &quot;MiSans Normal&quot;,
      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Normal.min.css&quot;,
      family: &quot;MiSans&quot;,
      weight: 400,
      display: &quot;swap&quot; as const,
    },
    // 小米字体 - MiSans Semibold
    &quot;misans-semibold&quot;: {
      id: &quot;misans-semibold&quot;,
      name: &quot;MiSans Semibold&quot;,
      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Semibold.min.css&quot;,
      family: &quot;MiSans&quot;,
      weight: 600,
      display: &quot;swap&quot; as const,
    },
  },
  fallback: [
    &quot;system-ui&quot;,
    &quot;-apple-system&quot;,
    &quot;BlinkMacSystemFont&quot;,
    &quot;Segoe UI&quot;,
    &quot;Roboto&quot;,
    &quot;sans-serif&quot;,
  ], // 全局字体回退
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从配置中可知，有两个结构需要去研究一下。&lt;/p&gt;
&lt;p&gt;字体选择配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 字体配置
export type FontConfig = {
  enable: boolean; // 是否启用自定义字体功能
  selected: string | string[]; // 当前选择的字体ID，支持单个或多个字体组合
  fonts: Record&amp;lt;string, FontItem&amp;gt;; // 字体库，以ID为键的对象
  fallback?: string[]; // 全局字体回退列表
  preload?: boolean; // 是否预加载字体文件以提高性能
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单个字体项配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 单个字体配置
export type FontItem = {
  id: string; // 字体唯一标识符
  name: string; // 字体显示名称
  src: string; // 字体文件路径或URL链接
  family: string; // CSS font-family 名称
  weight?: string | number; // 字体粗细，如 &quot;normal&quot;, &quot;bold&quot;, 400, 700 等
  style?: &quot;normal&quot; | &quot;italic&quot; | &quot;oblique&quot;; // 字体样式
  display?: &quot;auto&quot; | &quot;block&quot; | &quot;swap&quot; | &quot;fallback&quot; | &quot;optional&quot;; // font-display 属性
  unicodeRange?: string; // Unicode 范围，用于字体子集化
  format?:
    | &quot;woff&quot;
    | &quot;woff2&quot;
    | &quot;truetype&quot;
    | &quot;opentype&quot;
    | &quot;embedded-opentype&quot;
    | &quot;svg&quot;; // 字体格式，仅当 src 为本地文件时需要
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其对应的处理代码片段都在 &lt;code&gt;src/components/interactive/FootManager.astro&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;p&gt;CSS &lt;code&gt;font-family&lt;/code&gt; 属性的值是一个按照优先级顺序的字体名称列表，被称为&lt;strong&gt;字体栈&lt;/strong&gt;或&lt;strong&gt;字体族堆栈&lt;/strong&gt;。浏览器会从列表的最左边开始尝试，如果用户设备上没有安装该字体或者无法下载该字体，浏览器就会尝试列表中的下一个字体，以此类推。最后通常会以一个通用字体族作为后备，确保内容在任何情况下都基本可读。&lt;/p&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;code&gt;serif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;衬线体，字符笔画末端有装饰性的小细节。显得传统、正式、优雅&lt;/td&gt;
&lt;td&gt;Times New Roman, 宋体, SimSun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sans-serif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无衬线体，字符笔画末端干净，没有装饰。显得现代、简洁、易读&lt;/td&gt;
&lt;td&gt;Arial, Helvetica, 微软雅黑, Microsoft YaHei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monospace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等宽字体，所有字符宽度相同。常用于显示代码、终端文本&lt;/td&gt;
&lt;td&gt;Courier New, Consolas, Monaco&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cursive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;草书字体，模拟手写笔迹。具有艺术性，但可读性较差&lt;/td&gt;
&lt;td&gt;Comic Sans MS, 楷体 (有时)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fantasy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;奇幻字体，具有特殊艺术效果的字体。主要用于装饰&lt;/td&gt;
&lt;td&gt;Impact, Papyrus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统默认字体，直接使用用户操作系统的默认界面字体&lt;/td&gt;
&lt;td&gt;San Francisco (macOS), Segoe UI (Windows)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;至于 &lt;code&gt;fallback&lt;/code&gt; 字段里的其他值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-apple-system&lt;/code&gt;：专为 &lt;strong&gt;macOS&lt;/strong&gt; 和 &lt;strong&gt;iOS&lt;/strong&gt; 系统设计的字体家族名称，用于调用苹果系统的默认字体（San Francisco）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BlinkMacSystemFont&lt;/code&gt;：针对 macOS 上使用 &lt;strong&gt;Blink&lt;/strong&gt; 渲染引擎的浏览器（如 &lt;strong&gt;Chrome&lt;/strong&gt;、&lt;strong&gt;Edge&lt;/strong&gt;、&lt;strong&gt;Opera&lt;/strong&gt;）的字体名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Segoe UI&lt;/code&gt;：&lt;strong&gt;Misrosoft Windows&lt;/strong&gt; 从 &lt;strong&gt;Vista&lt;/strong&gt; 开始使用的默认界面字体&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Roboto&lt;/code&gt;：&lt;strong&gt;Google&lt;/strong&gt; 开发的 &lt;strong&gt;Android&lt;/strong&gt; 系统和 &lt;strong&gt;Chrome OS&lt;/strong&gt; 的默认字体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由此可见，&lt;code&gt;fallback&lt;/code&gt; 字段值的基本核心思想是选择对应平台上的默认系统字体。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;selected&lt;/code&gt; 字段主要作用是生成 CSS &lt;code&gt;font-family&lt;/code&gt; 属性的堆叠，期间它会过滤掉传入的 id，其对应的单个字体项中 &lt;code&gt;src&lt;/code&gt; 字段为空的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取选中的字体
const getSelectedFonts = () =&amp;gt; {
  if (!fontConfig.enable || !fontConfig.selected) return [];

  const selectedIds = Array.isArray(fontConfig.selected)
    ? fontConfig.selected
    : [fontConfig.selected];

  return selectedIds
    .map((id) =&amp;gt; fontConfig.fonts[id])
    .filter((font) =&amp;gt; font &amp;amp;&amp;amp; font.src); // 过滤掉系统字体和不存在的字体
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，目前填的 &lt;code&gt;selected: [&quot;system&quot;]&lt;/code&gt;，相当于 &lt;code&gt;selected: []&lt;/code&gt;，直接使用 &lt;code&gt;fallback&lt;/code&gt; 字段生成 &lt;code&gt;font-family&lt;/code&gt; 属性的值：&lt;code&gt;system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于成功被选择的每一个单个字体项，根据传入的 &lt;code&gt;src&lt;/code&gt; 字段的链接类型会生成不同类型的新标签：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 字体样式表链接 --&amp;gt;{
  selectedFonts.map((font) =&amp;gt; {
    // 判断是否为外部链接
    const isExternalUrl =
      font.src.startsWith(&quot;http://&quot;) ||
      font.src.startsWith(&quot;https://&quot;) ||
      font.src.startsWith(&quot;//&quot;);

    if (isExternalUrl) {
      // 外部字体链接 (如 Google Fonts, CDN等)
      return &amp;lt;link rel=&quot;stylesheet&quot; href={font.src} /&amp;gt;;
    } else {
      // 本地字体文件
      return (
        &amp;lt;style
          set:html={`
        @font-face {
          font-family: &quot;${font.family}&quot;;
          src: url(&quot;${font.src}&quot;) ${font.format ? `format(&quot;${font.format}&quot;)` : &quot;&quot;};
          ${font.weight ? `font-weight: ${font.weight};` : &quot;&quot;}
          ${font.style ? `font-style: ${font.style};` : &quot;&quot;}
          ${font.display ? `font-display: ${font.display};` : &quot;&quot;}
          ${font.unicodeRange ? `unicode-range: ${font.unicodeRange};` : &quot;&quot;}
        }
      `}
        /&amp;gt;
      );
    }
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单个字体项的各字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：不影响渲染。用于内部查找、CSS 类名生成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：不影响渲染。用于文档说明、可读性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;family&lt;/code&gt;：影响渲染。用于 CSS &lt;code&gt;font-family&lt;/code&gt; 属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;weight&lt;/code&gt;：仅影响渲染本地字体，由 &lt;code&gt;@font-face&lt;/code&gt; 规则定义&lt;/li&gt;
&lt;li&gt;&lt;code&gt;display&lt;/code&gt;：仅影响渲染本地字体，定义本地字体的加载行为，用于 CSS &lt;code&gt;font-display&lt;/code&gt; 属性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从结果上看，不管是使用外部字体链接还是本地字体文件，最终都会引入 &lt;code&gt;@font-face&lt;/code&gt; 规则。因为外部字体链接通常是一堆 &lt;code&gt;@font-face&lt;/code&gt; 规则的 CSS 文件的链接。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@font-face&lt;/code&gt; 规则允许使用自定义字体，需要在 CSS 文件的顶层定义或者放在 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 标签内，声明一个字体家族供后续使用。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;Zen Maru Gothic&apos;;
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/zenmarugothic/v19/o-0XIpIxzW5b-RxT-6A8jWAtCp-cQWpCOfKK_7mX3yPCWUgO7n9RJZk8vDuG3WM.2.woff2) format(&apos;woff2&apos;);
  unicode-range: U+ffd7, U+ffda-ffdc, U+ffe0-ffe2, U+ffe4, U+ffe6, U+ffe8-ffee, U+1f100-1f10c, U+1f110-1f16c, U+1f170-1f1ac, U+1f200-1f202, U+1f210-1f234;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键属性解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;font-family&lt;/code&gt;：自定义字体的别名。将在后面的 CSS 规则中用这个名字来引用它&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt;：字体文件的实际路径。可以通过 &lt;code&gt;url()&lt;/code&gt; 指定一个或多个源，浏览器会按顺序加载第一个它能识别的格式。&lt;code&gt;format()&lt;/code&gt; 用于帮助浏览器识别字体格式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;font-weight&lt;/code&gt;：告诉浏览器这个特定的字体文件对应哪种字重&lt;/li&gt;
&lt;li&gt;&lt;code&gt;font-style&lt;/code&gt;：告诉浏览器这个特定的字体文件是正常样式还是斜体&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unicode-range&lt;/code&gt;：指定一个作用范围 —— 只有当网页中的字符落在指定的 Unicode 编码范围内时，才会下载并使用这个特定的字体文件&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
如果只提供了一个字体文件（比如常规粗细 &lt;code&gt;400&lt;/code&gt;），但在 CSS 中使用了 &lt;code&gt;font-weight: 700&lt;/code&gt;，浏览器会尝试通过算法加粗这个字体。这种模拟效果很粗糙，远不如真正的粗体字文件美观。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CSS &lt;code&gt;font-display&lt;/code&gt; 是另一个关键属性，专门用于控制网页字体的加载和显示方式。&lt;/p&gt;
&lt;p&gt;当使用 &lt;code&gt;@font-face&lt;/code&gt; 引入一个网络字体时，浏览器可能的默认行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FOIT&lt;/strong&gt;（闪烁的不可见文本）：在自定义字体加载完成之前，浏览器会将文字隐藏，显示一片空白&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FOUT&lt;/strong&gt;（闪烁的无样式文本）：浏览器先使用备用字体显示文本，等自定义字体加载完成后再切换回来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;font-display&lt;/code&gt; 提供了一些值让我们去覆盖这种浏览器的默认行为。不过，为了理解这些值的含义，要先了解理解浏览器处理字体加载的三个关键时间段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阻塞期&lt;/strong&gt;：字体在加载时，浏览器会阻塞文本的显示，必须渲染不可见的备用字体。如果字体缓存中有，这个时期极其短&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换期&lt;/strong&gt;：如果阻塞期结束字体还没加载好，浏览器会使用备用字体。之后一旦自定义字体加载完成，就好立即交换过来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失效期&lt;/strong&gt;：如果交换期都结束了，字体还没加载好，浏览器就会放弃使用这个自定义字体，页面将一直使用备用字体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;font-display&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;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;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浏览器默认行为。通常是 FOIT（隐藏文本直到超时）&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;code&gt;block&lt;/code&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;code&gt;swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;立即用备用字体显示，等自定义字体加载好后立即交换&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;无限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fallback&lt;/code&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;code&gt;optional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文字会极短暂不可见，然后使用备用字体。是否交换取决于用户的网络连接和速度。如果用户在第一次访问时网络慢，字体可能永远不会加载&lt;/td&gt;
&lt;td&gt;极短&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;由此可见，&lt;code&gt;swap&lt;/code&gt; 是最常用、最推荐的选项，适用于我们这种正文等需要快速展示内容的情况。&lt;/p&gt;
&lt;p&gt;在了解了字体配置和显示的机制后，就可以修改字体的相关配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 字体配置
export const fontConfig = {
  enable: true, // 启用自定义字体功能
  preload: true, // 预加载字体文件以提高性能
-  selected: [&quot;system&quot;], // 当前选择的字体，支持多个字体组合
+  selected: [&quot;misans-light&quot;, &quot;misans-normal&quot;, &quot;misans-medium&quot;,  &quot;misans-semibold&quot;, &quot;misans-bold&quot;], // 当前选择的字体，支持多个字体组合
  fonts: {
-    // 系统字体
-    system: {
-      id: &quot;system&quot;,
-      name: &quot;系统字体&quot;,
-      src: &quot;&quot;, // 系统字体无需 src
-      family:
-        &quot;system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif&quot;,
-    },
-    // Google Fonts - Zen Maru Gothic
-    &quot;zen-maru-gothic&quot;: {
-      id: &quot;zen-maru-gothic&quot;,
-      name: &quot;Zen Maru Gothic&quot;,
-      src: &quot;https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@300;400;500;700;900&amp;amp;display=swap&quot;,
-      family: &quot;Zen Maru Gothic&quot;,
-      display: &quot;swap&quot; as const,
-    },
-    // Google Fonts - Inter
-    inter: {
-      id: &quot;inter&quot;,
-      name: &quot;Inter&quot;,
-      src: &quot;https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;amp;display=swap&quot;,
-      family: &quot;Inter&quot;,
-      display: &quot;swap&quot; as const,
-    },
+    // 小米字体 - MiSans Light
+    &quot;misans-light&quot;: {
+      id: &quot;misans-light&quot;,
+      name: &quot;MiSans Light&quot;,
+      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Light.min.css&quot;,
+      family: &quot;MiSans&quot;,
+      weight: 300,
+      display: &quot;swap&quot; as const,
+    },
    // 小米字体 - MiSans Normal
    &quot;misans-normal&quot;: {
      id: &quot;misans-normal&quot;,
      name: &quot;MiSans Normal&quot;,
      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Normal.min.css&quot;,
      family: &quot;MiSans&quot;,
      weight: 400,
      display: &quot;swap&quot; as const,
    },
+    // 小米字体 - MiSans Medium
+    &quot;misans-medium&quot;: {
+      id: &quot;misans-medium&quot;,
+      name: &quot;MiSans Medium&quot;,
+      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Medium.min.css&quot;,
+      family: &quot;MiSans&quot;,
+      weight: 500,
+      display: &quot;swap&quot; as const,
+    },
    // 小米字体 - MiSans Semibold
    &quot;misans-semibold&quot;: {
      id: &quot;misans-semibold&quot;,
      name: &quot;MiSans Semibold&quot;,
      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Semibold.min.css&quot;,
      family: &quot;MiSans&quot;,
      weight: 600,
      display: &quot;swap&quot; as const,
    },
+    // 小米字体 - MiSans Bold
+    &quot;misans-bold&quot;: {
+      id: &quot;misans-bold&quot;,
+      name: &quot;MiSans Bold&quot;,
+      src: &quot;https://unpkg.com/misans@4.1.0/lib/Normal/MiSans-Bold.min.css&quot;,
+      family: &quot;MiSans&quot;,
+      weight: 700,
+      display: &quot;swap&quot; as const,
+    },
  }
  fallback: [
    &quot;system-ui&quot;,
    &quot;-apple-system&quot;,
    &quot;BlinkMacSystemFont&quot;,
    &quot;Segoe UI&quot;,
    &quot;Roboto&quot;,
    &quot;sans-serif&quot;,
  ], // 全局字体回退
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就确保所选用的自定义字体涵盖了中文、英文、数字及日文字符集，并包含了常用的 300 到 700 字重。&lt;/p&gt;
&lt;p&gt;在调试的过程中，我注意到 Inter 是一款可变字体，只需加载一个智能的字体文件，就可以让浏览器通过算法“生成”指定范围内的任何字重。其 &lt;code&gt;@font-face&lt;/code&gt; 规则里的 &lt;code&gt;font-weight&lt;/code&gt; 不管是多少都是指向同一个字体文件——Inter-Thin。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
在调试字体显示时，可按 &lt;code&gt;F12&lt;/code&gt; 键打开浏览器的&lt;code&gt;开发人员工具&lt;/code&gt;，切换到&lt;code&gt;元素&lt;/code&gt;面板，接着按 &lt;code&gt;Ctrl+Shift+C&lt;/code&gt; 键（或点击左上角的“选择元素”图标），在页面上点击要检查的元素。随后，在开发人员工具右下角找到并展开&lt;code&gt;已计算&lt;/code&gt;面板，滚动到底部即可查看&lt;code&gt;呈现的字体&lt;/code&gt;信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;友链&lt;/h2&gt;
&lt;p&gt;Firefly 主题内置并默认开启友链页面。友链（友情链接）页面，初看只是列出跟本博客相关或有过联系的网站链接，从而提升对方网站的曝光率。但我在 AI 上询问得到了令人惊奇的回答，其中，对博主自身的好处有：&lt;/p&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;潜在的流量回流和 SEO 益处&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;friendsConfig.ts&lt;/code&gt; 文件用于配置友链页面下的友链，例如可以添加 &lt;a href=&quot;https://github.com/matsuzaka-yuki/Mizuki&quot;&gt;Mizuki&lt;/a&gt; 和 &lt;a href=&quot;https://github.com/saicaca/fuwari&quot;&gt;fuwari&lt;/a&gt; 这两个跟主题相关的友情链接。&lt;/p&gt;
&lt;p&gt;我在文件里添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // ...
  {
    &quot;title&quot;: &quot;Mizuki&quot;,
    imgurl: &quot;https://mizuki.mysqil.com/_astro/avatar.Nvq3-FmN_Z28HWf5.webp&quot;,
    desc: &quot;融合Android原生质感与二次元美学：Material Design 3规范下的Astro博客主题（Tailwind CSS实现）&quot;,
    siteurl: &quot;https://github.com/matsuzaka-yuki/Mizuki&quot;,
    tags: [&quot;GitHub&quot;, &quot;Theme&quot;],
    weight: 9,
    enabled: true,
  },
  {
    title: &quot;fuwari&quot;,
    imgurl: &quot;https://fuwari.vercel.app/_astro/demo-avatar.CxcI0ivM_1nbuVe.webp&quot;,
    desc: &quot;✨A static blog template built with Astro.&quot;,
    siteurl: &quot;https://github.com/saicaca/fuwari&quot;,
    tags: [&quot;GitHub&quot;, &quot;Theme&quot;],
    weight: 9,
    enabled: true,
  },
  // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;友链页面下面的自定义内容在 &lt;code&gt;src/content/spec/friends.md&lt;/code&gt; 文件里，稍微修改一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
- 站点名称: 夏夜流萤
- 站点描述: 飞萤之火自无梦的长夜亮起，绽放在终竟的明天。
- 站点链接: https://blog.cuteleaf.cn
+ 站点名称: 未来之蓝
+ 站点描述: 心怀蔚蓝愿景，脚踏实地前行。每一篇记录都是通向理想未来的坚实脚印。
+ 站点链接: https://xpfxzxc.github.io
头像链接: https://q1.qlogo.cn/g?b=qq&amp;amp;nk=7618557&amp;amp;s=640

## ✉️申请友链

- 请将您的网站信息发送邮件至：`xxx@xxx.com`
+ 请将您的网站信息发送邮件至：`xpfxzxc@gmail.com`
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;追番&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bangumi.tv/&quot;&gt;&lt;strong&gt;Bangumi&lt;/strong&gt;&lt;/a&gt;，名字来源于日语的“番组”（节目），致力于让阿宅们拥有一个轻松便捷独特的交流与沟通环境。目前设有五大分区：动画、书籍、音乐、游戏、三次元。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;siteConfig.ts&lt;/code&gt; 文件里可以配置 Bangumi 用户 ID，用于在追番页面下显示该用户的收藏列表。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;bangumi&lt;/code&gt; 字段下的 &lt;code&gt;userId&lt;/code&gt; 的值改为自己的 Bangumi 用户 ID 即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 追番配置
bangumi: {
-  userId: &quot;1163581&quot;, // 在此处设置你的Bangumi用户ID
+  userId: &quot;1175686&quot;, // 在此处设置你的Bangumi用户ID
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;赞助&lt;/h2&gt;
&lt;p&gt;赞助页面为博主提供一个渠道，来接受读者自愿性、直接的财务支持，通过支付宝/微信的一次性赞助，或者像&lt;strong&gt;爱发电&lt;/strong&gt;等平台的定期赞助等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sponsorConfig.ts&lt;/code&gt; 文件可以对赞助页面进行配置，需要将 &lt;code&gt;public/assets/images/sponsor&lt;/code&gt; 目录下的 &lt;code&gt;alipay.png&lt;/code&gt; 和 &lt;code&gt;wechat.png&lt;/code&gt; 二维码图片替换成自己的收款二维码图片。&lt;/p&gt;
&lt;p&gt;不过，微信 APP 和淘宝 APP 默认生成的收款二维码中自带头像。如果觉得这些收款二维码中的头像不好看，可以借助工具先解码二维码再重新生成二维码。更进一步的，甚至可以自定义二维码样式。&lt;/p&gt;
&lt;p&gt;有两个网站用于方便解码和自定义生成二维码图片：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tool.lu/qrcode&quot;&gt;在线生成二维码 - 在线工具&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ma3you.com&quot;&gt;码上游&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果二维码的尺寸较大，可以使用 ImageMagick 工具去调整大小，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick wechat.png -resize 400x400 -filter Lanczos wechat.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据实际需求，我将 &lt;code&gt;sponsorConfig.ts&lt;/code&gt; 文件内容改成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { SponsorConfig } from &quot;../types/config&quot;;

export const sponsorConfig: SponsorConfig = {
  title: &quot;&quot;, // 页面标题，如果留空则使用 i18n 中的翻译
  description:
    &quot;&quot;, // 页面描述文本，如果留空则使用 i18n 中的翻译
  usage:
-    &quot;您的赞助将用于服务器维护、内容创作和功能开发，帮助我持续提供优质内容。&quot;, // 赞助用途说明
+    &quot;您的赞助将用于内容创作，帮助我持续提供优质内容。&quot;, // 赞助用途说明
  // 是否显示赞助者列表
  showSponsorsList: true,
  // 是否在文章详情页底部显示赞助按钮
  showButtonInPost: true,

  // 赞助方式列表
  methods: [
    {
      name: &quot;支付宝&quot;,
      icon: &quot;fa6-brands:alipay&quot;,
      qrCode: &quot;/assets/images/sponsor/alipay.png&quot;, // 收款码图片路径（需要放在 public 目录下）
      link: &quot;&quot;,
      description: &quot;使用 支付宝 扫码赞助&quot;,
      enabled: true,
    },
    {
      name: &quot;微信&quot;,
      icon: &quot;fa6-brands:weixin&quot;,
      qrCode: &quot;/assets/images/sponsor/wechat.png&quot;, // 收款码图片路径
      link: &quot;&quot;,
      description: &quot;使用 微信 扫码赞助&quot;,
      enabled: true,
    },
-    {
-      name: &quot;爱发电&quot;,
-      icon: &quot;simple-icons:afdian&quot;,
-      qrCode: &quot;&quot;,
-      link: &quot;https://afdian.com/a/cuteleaf&quot;,
-      description: &quot;通过 爱发电 进行赞助&quot;,
-      enabled: true,
-    },
-    {
-      name: &quot;Github&quot;,
-      icon: &quot;fa6-brands:github&quot;,
-      qrCode: &quot;&quot;,
-      link: &quot;https://github.com/CuteLeaf/Firefly&quot;,
-      description: &quot;点个Star就是最大的支持&quot;,
-      enabled: true,
-    },
  ],

  // 赞助者列表（可选）
  sponsors: [
-    // 示例：已实名赞助者
-    {
-      name: &quot;夏叶&quot;,
-      amount: &quot;¥50&quot;,
-      date: &quot;2025-10-01&quot;,
-      message: &quot;感谢分享！&quot;,
-    },
-    // 示例：匿名赞助者
-    {
-      name: &quot;匿名用户&quot;,
-      amount: &quot;¥20&quot;,
-      date: &quot;2025-10-01&quot;,
-    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于我&lt;/h2&gt;
&lt;p&gt;“关于我”页面就是博客里专门用来介绍“我是谁、为什么写这个博客、在做什么”的地方。&lt;/p&gt;
&lt;p&gt;可在 &lt;code&gt;src/content/spec/about.md&lt;/code&gt; 文件里修改“关于我”页面的内容。&lt;/p&gt;
&lt;p&gt;我将该文件内容改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 关于我 / About Me

-你好！我是 **夏叶** ，一个在数字世界中默默无闻的一片叶子。
+欢迎来到我的小角落。这里，主要是我写给自己看的一些文字和记录。很高兴你路过这里，并驻足阅读。
+
+我是 **xpfxzxc**。这个昵称很久前就开始使用了，但我忘记了这个昵称如何是取的，这个昵称可能没有什么含义吧。
+
+我是一个 98 后的听障人，不擅长言语沟通，头脑反应略慢，没创作灵感，平时喜欢独来独往、在网上冲浪。没有什么特长，唯一我比较懂的就是编程和游戏了，但在圈子里算是在底层水平，在日常上也不太拿得出手。我对编程曾充满热情，现在有点生疏，仍然“玩”得不太懂，但不想放弃。
+
+建立这个博客的初衷是我单纯地想试试部署好看的个人博客，还有记录以后学习到的技能知识。有时头脑里会有较多的想法，此时如果能够主动去梳理思路并记录下来，对当前和以后都会颇有收获。
+
+这里可能没有干货和指南，只有非常个人的、不成熟的想法和个人学习记录。如果你在我的文字里，找到了一丝共鸣，或者从中受到启发，或者从中成功解决某些疑惑，那将是我莫大的荣幸。
+
+欢迎你在想说话的时候，通过评论或邮件与我分享你的感受。我不一定会及时回复，但会认真阅读。

## 🛠️ 关于本站

-这个网站使用 **Astro** 框架构建，采用了 [Firefly](https://github.com/CuteLeaf/Firefly)模板
+这个网站使用 **Astro** 框架构建，采用了 [Firefly](https://github.com/CuteLeaf/Firefly) 主题。


**Firefly** 是一款基于 Astro 框架开发的清新美观且现代化个人博客主题，专为技术爱好者和内容创作者设计。该主题融合了现代 Web 技术栈，提供了丰富的功能模块和高度可定制的界面，让您能够轻松打造出专业且美观的个人博客网站。

-
-**🖥️在线预览： [Firefly - Demo site](https://firefly.cuteleaf.cn/)**
-
-**🏠我的博客： [https://blog.cuteleaf.cn](https://blog.cuteleaf.cn/)**
-
-**📝Firefly使用文档： [https://docs-firefly.cuteleaf.cn](https://docs-firefly.cuteleaf.cn/)**
-
-**⭐Firefly开源地址：https://github.com/CuteLeaf/Firefly** 
-
::github{repo=&quot;CuteLeaf/Firefly&quot;}

-&amp;lt;img src=&quot;/assets/images/firefly.png&quot; /&amp;gt;
-
-

## 📫 联系方式

如果你想和我交流技术问题，分享有趣的想法，或者只是想打个招呼，欢迎通过以下方式联系我：

-* 💻 **GitHub**: [CuteLeaf](https://github.com/CuteLeaf)
-* ✉️ **Email**: [xiaye@msn.com](mailto:xiaye@msn.com)
+* 💻 **GitHub**：[xpfxzxc](https://github.com/xpfxzxc)
+* ✉️ **Email**：[xpfxzxc@gmail.com](mailto:xpfxzxc@gmail.com)

---

-*感谢你的来访！希望在这里能找到对你有用的内容。Firefly博客系统完全开源，如果喜欢的话，不妨给个GitHub点个Star ⭐ 支持一下！*
+*感谢你的来访！希望在这里能找到对你有用的内容。如果想感谢或者想支持我继续创作，可以访问我的赞助页面。*

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;广告&lt;/h2&gt;
&lt;p&gt;广告栏内容的相关配置在 &lt;code&gt;adConfig.ts&lt;/code&gt; 文件中。如需启用广告栏组件，则需在 &lt;code&gt;sidebarConfig.ts&lt;/code&gt; 文件中进行相应配置。与站点公告类似，广告内容不一定局限于“广告”形式，例如也可以用于引导访客前往赞助页面。&lt;/p&gt;
&lt;p&gt;我将相关配置改成引导访客前往赞助页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
// 广告配置2 - 完整内容广告
export const adConfig2: AdConfig = {
  title: &quot;支持博主&quot;,
  content:
-    &quot;如果您觉得本站内容对您有帮助，欢迎支持我们的创作！您的支持是我们持续更新的动力。&quot;,
+    &quot;如果您觉得本站内容对您有帮助，欢迎支持我的创作！您的支持将激励我持续提供优质内容。&quot;,
  image: {
    src: &quot;/assets/images/d2.webp&quot;,
    alt: &quot;支持博主&quot;,
-    link: &quot;about/&quot;,
+    link: &quot;/sponsor/&quot;,
    external: false,
  },
  link: {
    text: &quot;支持一下&quot;,
-    url: &quot;about/&quot;,
+    url: &quot;/sponsor/&quot;,
    external: false,
  },
  closable: true,
  displayCount: -1,
  padding: {
    // all: &quot;1rem&quot;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后开启该广告栏组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  // 组件类型：广告栏组件 2
  type: &quot;advertisement&quot;,
  // 是否启用该组件
-  enable: false,
+  enable: true
  // 组件显示顺序
  order: 7,
  // 组件位置：&quot;sticky&quot; 表示粘性定位
  position: &quot;sticky&quot;,
  // CSS 类名
  class: &quot;onload-animation&quot;,
  // 动画延迟时间
  animationDelay: 350,
  // 配置ID：使用第二个广告配置
  configId: &quot;ad2&quot;,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;完成了这些配置，个人博客便焕然一新，颇具个人风格了。接下来即可将变动提交并推送至线上。在此过程中，最好遵循 &lt;strong&gt;Git&lt;/strong&gt; 最佳实践中的&lt;strong&gt;原子性提交&lt;/strong&gt;原则，即每个 Commit 只包含单一、完整的逻辑变更，目的清晰，以避免产生中间状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;元数据配置&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;目前，&lt;code&gt;siteConfig.ts&lt;/code&gt; 文件混杂着多个模块的改动，不适合一股脑全提交。虽然可以“倒回去分步提交”，但是有更省力的办法：用 &lt;code&gt;git add -p &amp;lt;文件名&amp;gt;&lt;/code&gt; 交互式分块暂存，将一个文件的多处改动拆开，分批提交。&lt;/p&gt;
&lt;p&gt;Git 会逐块显示差异，并询问如何处理每一块，例如：&lt;code&gt;(1/5) Stage this hunk [y,n,q,a,d,j,J,g,/,s,e,p,?]?&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;code&gt;y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;接受（暂存）当前块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;跳过当前块（不暂存）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将当前块进一步拆分成更小的块（如果可拆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;手动编辑当前块（高级用法，需熟悉 diff 格式）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;退出，不再处理后续块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示帮助信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;git add -p src/config/siteConfig.ts
git commit -m &quot;chore: 更新站点元数据（标题、描述、关键词）&quot;
git add public/assets/images/favicon.ico
git commit -m &quot;chore: 更新 Favicon 图标&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;站点 Logo 和名称&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -p src/config/siteConfig.ts
git rm public/assets/images/LiuYingPure3.svg
git add public/assets/images/logo.ico
git commit -m &quot;chore: 更新站点 Logo 和名称&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;个人基本资料&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/profileConfig.ts
git add public/assets/images/avatar.webp
git commit -m &quot;feat: 完善个人基本资料（头像、名称、介绍、社交媒体链接）&quot; -m &quot;- 更新个人基本信息（名称、介绍）&quot; -m &quot;替换个人头像图片&quot; -m &quot;调整现有社交媒体链接&quot; -m &quot;新增两个社交媒体平台链接&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;站点公告&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/announcementConfig.ts
git commit -m &quot;feat: 更新站点公告配置&quot; -m &quot;- 更新公告标题和内容文案&quot; -m &quot;- 设置为不可关闭的永久显示模式&quot; -m &quot;- 移除公告链接按钮功能&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;导航栏&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/navBarConfig.ts
git commit -m &quot;feat: 更新导航栏&quot; -m &quot;- 调整现有导航链接地址&quot; -m &quot;- 新增两个导航链接按钮&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;背景壁纸&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -p src/config/siteConfig.ts
git add public/assets/images/d1.webp
git add public/assets/images/m1.webp
git commit -m &quot;feat: 完善背景壁纸显示效果&quot; -m &quot;- 替换桌面端和移动端背景壁纸图片资源&quot; -m &quot;- 调整横幅模式下图片位置和主副标题内容&quot; -m &quot;- 优化全屏壁纸模式下遮罩层模糊程度&quot; -m &quot;- 完善壁纸来源信息显示设置&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主题色&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -p src/config/siteConfig.ts
git commit -m &quot;feat: 更新主题色配置&quot; -m &quot;- 调整主题色的颜色&quot; -m &quot;- 锁定主题色选择器，对访客隐藏&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;字体&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/fontConfig.ts
git commit -m &quot;feat: 更新字体配置&quot; -m &quot;- 调整 font-family 字体栈&quot; -m &quot;- 引入新的 @font-face 字体规则&quot; -m &quot;- 替换默认字体风格&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;友链&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/friendsConfig.ts
git add src/content/spec/friends.md
git commit -m &quot;feat: 更新友链页面内容&quot; -m &quot;- 修改友链页面介绍文本内容&quot; -m &quot;- 新增两个友链条目&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;追番&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/siteConfig.ts
git commit -m &quot;chore: 更新 Bangumi 用户 ID&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;赞助&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/sponsorConfig.ts
git add public/assets/images/sponsor/alipay.png
git add public/assets/images/sponsor/wechat.png
git commit -m &quot;chore: 更新赞助页面信息和支付码&quot; -m &quot;- 调整赞助说明内容&quot; -m &quot;- 更新赞助方式列表&quot; -m &quot;- 替换支付宝和微信支付二维码&quot; -m &quot;- 清空赞助者列表&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关于我&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/content/spec/about.md
git commit -m &quot;chore: 更新关于我页面内容&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;广告&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add src/config/adConfig.ts
git add src/config/sidebarConfig.ts
git add public/assets/images/d2.webp
git commit -m &quot;feat: 启用赞助引导广告栏&quot; -m &quot;- 开启赞助引导广告栏组件显示&quot; -m &quot;- 更新引导文案内容&quot; -m &quot;- 调整图片和按钮链接指向赞助页面&quot; -m &quot;- 替换展示图片&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，将代码推送到远程仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin custom
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>基于 Firefly 主题的 Astro 博客部署（二）：文章发布</title><link>https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part2/</link><guid isPermaLink="true">https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part2/</guid><description>记录了自己在基于 Firefly 主题的 Astro 博客上，对文章文件各要素的探索与简单总结，并接触到 Front matter 和 Markdown 扩展语法。</description><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在成功将博客站点部署到 &lt;strong&gt;GitHub Pages&lt;/strong&gt; 上后，我打算暂不修改博客的个人资料（如个人名称、头像、介绍、公告、背景图等），而是先撰写文章再发布，所以首先研究一下这部分。&lt;/p&gt;
&lt;h2&gt;探索&lt;/h2&gt;
&lt;p&gt;在博客主页中可以看到，刚部署后的博客自带了几篇示例文章。这些示例文章是 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot;&gt;&lt;strong&gt;Firefly&lt;/strong&gt;&lt;/a&gt; 主题作者特意留下的，里面讲解了在撰写文章时可以用的 &lt;strong&gt;Markdown&lt;/strong&gt; 语法。&lt;/p&gt;
&lt;h3&gt;文章位置&lt;/h3&gt;
&lt;p&gt;随便截取其中一篇示例文章中的几个字，然后在 &lt;strong&gt;VSCode&lt;/strong&gt; 中按快捷键 &lt;code&gt;Ctrl+Shift+F&lt;/code&gt; 或点击左侧栏 🔍 打开左侧栏的&lt;code&gt;搜索栏&lt;/code&gt;，将几个字粘贴到&lt;code&gt;搜索框&lt;/code&gt;中，很快就能够在 VSCode 左侧看见示例文章在项目中的具体位置：在 &lt;code&gt;src/content/posts&lt;/code&gt; 目录下。&lt;/p&gt;
&lt;p&gt;该目录下存放了所有文章，文章也可以存放在子目录中。文章在 &lt;code&gt;src/content/posts&lt;/code&gt; 目录下的相对路径决定其最终的 URL。例如，文件 &lt;code&gt;src/content/posts/firefly/hello-world.md&lt;/code&gt; 对应的访问 URL 为：&lt;code&gt;https://&amp;lt;username&amp;gt;.github.io/posts/firefly/hello-world&lt;/code&gt;（&lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; 需替换成 &lt;strong&gt;GitHub&lt;/strong&gt; 用户名，下文同理）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
创建子目录可以更好地组织文章和资源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;文章组成&lt;/h3&gt;
&lt;p&gt;每个文章文件的内容由两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;元数据&lt;/strong&gt;：位于文章文件的开头，为 Markdown 文件本身提供了结构化信息（&lt;strong&gt;YAML&lt;/strong&gt; 格式），由一对 &lt;code&gt;---&lt;/code&gt; 分隔符包裹着&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文章主体&lt;/strong&gt;：Markdown 格式的正文内容，紧接在元数据下方&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;元数据&lt;/h3&gt;
&lt;p&gt;从各个示例文章文件里，可以总结出元数据中可使用的字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;（&lt;strong&gt;必填&lt;/strong&gt;）：文章标题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;published&lt;/code&gt;（&lt;strong&gt;必填&lt;/strong&gt;）：文章发布日期，日期格式 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updated&lt;/code&gt;：文章的最后一次更新日期，日期格式 &lt;code&gt;YYYY-MM-DD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pinned&lt;/code&gt;：将文章置顶在文章列表顶部&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;：文章的简短描述，显示在文章列表里&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;：文章封面图片路径，有三种格式：
&lt;ul&gt;
&lt;li&gt;以 &lt;code&gt;http&lt;/code&gt; 或 &lt;code&gt;https&lt;/code&gt; 开头：网络图片&lt;/li&gt;
&lt;li&gt;以 &lt;code&gt;/&lt;/code&gt; 开头：&lt;code&gt;public&lt;/code&gt; 目录下的图片&lt;/li&gt;
&lt;li&gt;不带任何前缀：相对于 Markdown 文件的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tags&lt;/code&gt;：文章标签，字符串数组格式，例如：&lt;code&gt;[&quot;技术&quot;, &quot;随笔&quot;]&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;licenseName&lt;/code&gt;：文章内容的许可协议的显示名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;licenseUrl&lt;/code&gt;：文章内容的许可协议 URL&lt;/li&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt;：文章作者&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceLink&lt;/code&gt;：文章内容的来源链接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;draft&lt;/code&gt;：将文章标记为草稿。设为 &lt;code&gt;true&lt;/code&gt; 后，文章仅在开发环境下可见，不会包含在最终发布的网站中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slug&lt;/code&gt;：自定义文章 URL 的末尾路径。如果未设置，将默认使用文章在 &lt;code&gt;posts&lt;/code&gt; 目录下的相对路径（不含 &lt;code&gt;.md&lt;/code&gt; 扩展名）作为路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lang&lt;/code&gt;：仅当文章语言与 &lt;code&gt;config.ts&lt;/code&gt; 文件中的网站语言不同时需要设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;字段的值即为该行冒号（：）之后的所有内容，因此通常无需使用引号。&lt;/p&gt;
&lt;p&gt;以下是一篇示例文章中的片段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Firefly 简单使用指南
published: 2025-10-11
pinned: true
description: &quot;如何使用 Firefly 博客模板。&quot;
image: &quot;./cover.webp&quot;
tags: [&quot;Firefly&quot;, &quot;博客&quot;, &quot;Markdown&quot;, &quot;使用指南&quot;]
category: 博客指南
draft: true
---

这个博客模板是基于 [Astro](https://astro.build/) 构建的。对于本指南中未提及的内容，您可以在 [Astro 文档](https://docs.astro.build/) 中找到答案。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;了解了以上信息后，就可以开始撰写文章并发布了。不过，我打算先将自带的示例文章标记为草稿，保留而不是删除这些示例文章。这样，如果以后在撰写文章的过程中，一时想不起元数据或 Markdown 语法，那么可以在这些示例文章中查阅。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;h4&gt;1. 标记示例文章为草稿&lt;/h4&gt;
&lt;p&gt;用 VSCode 打开本地项目，手动将在 &lt;code&gt;src/content/posts&lt;/code&gt; 目录下的所有示例文章文件中元数据里的 &lt;code&gt;draft: false&lt;/code&gt; 改为 &lt;code&gt;draft: true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;接着在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;chore: 标记自带的文章为草稿&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 撰写文章&lt;/h4&gt;
&lt;p&gt;注意到项目目录下的 &lt;code&gt;package.json&lt;/code&gt; 文件里有可执行的脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;new-post&quot;: &quot;node scripts/new-post.js&quot;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开 &lt;code&gt;scripts/new-post.js&lt;/code&gt; 文件，研究代码后知道：它接收文件名参数，并在 &lt;code&gt;src/content/posts&lt;/code&gt; 目录下生成仅包含元数据模板的文章文件。&lt;/p&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm run new-post astro-firefly-blog-deploy/part1.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将在 &lt;code&gt;src/content/posts&lt;/code&gt; 目录下创建子目录 &lt;code&gt;astro-firefly-blog-deploy&lt;/code&gt;，并在该子目录下创建文章文件 &lt;code&gt;part1.md&lt;/code&gt;。这文章文件包含着初始元数据模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: part1.md
published: 2025-10-30
description: &apos;&apos;
image: &apos;&apos;
tags: []
category: &apos;&apos;
draft: false 
lang: &apos;&apos;
---

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 URL 路径 &lt;code&gt;/posts/astro-firefly-blog-deploy/part1&lt;/code&gt; 访问到该文章。&lt;/p&gt;
&lt;p&gt;为了避免使用嵌套过多的 URL 子路径，添加 &lt;code&gt;slug: astro-firefly-blog-deploy-part1&lt;/code&gt; 到元数据块里。这样，可以通过 URL 路径 &lt;code&gt;/posts/astro-firefly-blog-deploy-part1&lt;/code&gt; 访问到该文章。&lt;/p&gt;
&lt;h4&gt;3. 发布文章&lt;/h4&gt;
&lt;p&gt;修改元数据，并撰写文章主体后，就可以将文章发布出去了。&lt;/p&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;feat: 添加文章 &apos;基于 Firefly 主题的 Astro 博客部署（一）：从零到 GitHub Pages&apos;&quot;
git push origin custom
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;扩展内容：Front Matter&lt;/h2&gt;
&lt;p&gt;在与 AI 沟通元数据相关内容的时候，注意到 AI 对这些元数据的格式和使用的单词颇为熟悉。经过深入询问才知道：其实前面所说的元数据有个标准名称叫做 &lt;strong&gt;Front Matter&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;Front Matter 是位于文件（通常是 Markdown 或 &lt;strong&gt;HTML&lt;/strong&gt; 文件）顶部区域的一段特定格式（通常是 YAML 格式）的代码块，文件正文就写在这个 Front Matter 块的下方。&lt;/p&gt;
&lt;h3&gt;作用&lt;/h3&gt;
&lt;p&gt;用于为文章或页面定义元数据，告知静态网站生成器如何处理这些文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容和页面分离：无需改动模板代码，就能通过简单的键值对控制每个页面的行为和展示方式&lt;/li&gt;
&lt;li&gt;驱动网站生成
&lt;ul&gt;
&lt;li&gt;为文章排序（根据 &lt;code&gt;date&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;生成标签页和分类页（根据 &lt;code&gt;tags&lt;/code&gt; 和 &lt;code&gt;category&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;在列表中显示文章标题、摘要和封面（根据 &lt;code&gt;title&lt;/code&gt;、&lt;code&gt;description&lt;/code&gt; 和 &lt;code&gt;image&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;控制构建行为：像 &lt;code&gt;draft: true&lt;/code&gt; 这样的键值对告知生成器：“这篇文章是草稿，在正式构建时请忽略它”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;扩展内容：从文章文件到文章展示&lt;/h2&gt;
&lt;p&gt;已经知道 Firefly 主题项目里 &lt;code&gt;src/content/posts&lt;/code&gt; 目录存放着文章内容。在构建静态站点时，Astro 会为非草稿文章逐一生成对应的 HTML 页面和路由。要进一步了解这个过程是如何在代码中体现的，就需要对 Astro 框架的相关知识有所了解。&lt;/p&gt;
&lt;h3&gt;官方文档相关资料&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/basics/astro-pages/&quot;&gt;页面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/routing/&quot;&gt;路由&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/basics/astro-components/&quot;&gt;组件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/&quot;&gt;内容集合&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/reference/astro-syntax/&quot;&gt;模板表达式参考&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/upgrade-to/v5/#legacy-v20-content-collections-api&quot;&gt;旧版：v2.0 内容集合 API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解析项目代码&lt;/h3&gt;
&lt;p&gt;接下来，将结合 Astro 的相关知识点，来解析项目代码。&lt;/p&gt;
&lt;h4&gt;页面文件&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;src/pages&lt;/code&gt; 目录下的文件（支持 &lt;code&gt;.astro&lt;/code&gt;、&lt;code&gt;.ts&lt;/code&gt; 等格式）会被 Astro 自动识别为&lt;strong&gt;页面&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面展示了项目 &lt;code&gt;src/pages&lt;/code&gt; 目录的实际结构，其中的每一个文件都对应一个或多个页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/pages/
├── 404.astro
├── [...page].astro
├── about.astro
├── anime.astro
├── archive.astro
├── friends.astro
├── og/
│   └── [...slug].png.ts
├── posts/
│   └── [...slug].astro
├── robots.txt.ts
├── rss.astro
└── rss.xml.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;静态路由&lt;/h4&gt;
&lt;p&gt;Astro 使用基于文件的路由，根据项目 &lt;code&gt;src/pages&lt;/code&gt; 目录下的文件结构来构建链接，自动将它们作为网站页面。&lt;/p&gt;
&lt;p&gt;这意味着，虽然 Astro 项目没有单独的路由配置，但依然可以通过页面文件在 &lt;code&gt;src/pages&lt;/code&gt; 目录中的路径推断出最终生成的路由。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysite.com/about -&amp;gt; src/pages/about.astro
mysite.com/404   -&amp;gt; src/pages/404.astro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：像 &lt;code&gt;[...slug].astro&lt;/code&gt; 这样采用特殊命名的页面文件属于&lt;strong&gt;动态路由&lt;/strong&gt;，不属于&lt;strong&gt;静态路由&lt;/strong&gt;的范畴。&lt;/p&gt;
&lt;h4&gt;组件脚本&lt;/h4&gt;
&lt;p&gt;以 &lt;code&gt;.astro&lt;/code&gt; 为后缀的文件是 Astro 组件，每个 Astro 组件由&lt;strong&gt;组件脚本&lt;/strong&gt;和&lt;strong&gt;组件模板&lt;/strong&gt;两部分组成。&lt;/p&gt;
&lt;p&gt;Astro 使用代码围栏 &lt;code&gt;---&lt;/code&gt; 来识别组件脚本部分。虽然这种语法形式与 Front Matter 相似，但是两者作用不同：组件脚本内编写的是 &lt;strong&gt;TypeScript&lt;/strong&gt; 代码，而非元数据。&lt;/p&gt;
&lt;p&gt;后续会提到组件脚本中的一些作用和组件模板的一些语法。&lt;/p&gt;
&lt;h4&gt;动态路由&lt;/h4&gt;
&lt;p&gt;类似 &lt;code&gt;[...slug].astro&lt;/code&gt; 这样文件名中包含方括号的 Astro 组件用于实现动态路由。这类文件根据传入的动态参数，将文件名内方括号对应的部分替换为具体的参数值，生成对应的多个路由和匹配页面。&lt;/p&gt;
&lt;p&gt;例如，对于 &lt;code&gt;src/pages/posts/[category]/[post].astro&lt;/code&gt; 文件，当传入参数 &lt;code&gt;{ category: &apos;demo&apos;, post: &apos;hello-world&apos; }&lt;/code&gt; 时，将会生成如下路由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysite.com/pages/posts/demo/hello-world -&amp;gt; src/pages/posts/[category]/[post].astro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;剩余参数&lt;/strong&gt;（例如：&lt;code&gt;[...slug]&lt;/code&gt;）可以匹配任何深度的文件路径。&lt;/p&gt;
&lt;p&gt;项目中 &lt;code&gt;src/pages/posts&lt;/code&gt; 目录下的 &lt;code&gt;[...slug].astro&lt;/code&gt; 文件，自然地令人联想到文章页面的生成和路由跟这页面文件有关，且动态路由参数的值与 &lt;code&gt;slug&lt;/code&gt; 的值有关。&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;静态站点生成&lt;/strong&gt;模式下，因为必须在构建时确定所有路由。所以，在组件的脚本部分中，需要通过导出 &lt;code&gt;getStaticPaths()&lt;/code&gt; 函数来指定所有可能的参数组合。该函数返回一个对象数组，其中每个对象的 &lt;code&gt;param&lt;/code&gt; 字段定义了 &lt;code&gt;slug&lt;/code&gt; 参数的具体取值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
export async function getStaticPaths() {
  const blogEntries = await getSortedPosts();
  return blogEntries.map((entry) =&amp;gt; ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这代码片段对应了之前提到的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过修改文章元数据里的 &lt;code&gt;slug&lt;/code&gt; 字段值，能够更改文章的访问路径&lt;/li&gt;
&lt;li&gt;组件脚本的作用之一是通过导出特定函数，向框架提供所需信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;内容集合&lt;/h4&gt;
&lt;p&gt;根据前面的分析，文章内容存储在 &lt;code&gt;src/pages&lt;/code&gt; 目录之外，这意味着默认情况下不会为内容生成页面和路由。文章页面的生成好像跟 &lt;code&gt;src/content/posts&lt;/code&gt; 目录无关。那么，Astro 是如何定位文章文件、并为每篇文章生成对应的页面和路由的呢？&lt;/p&gt;
&lt;p&gt;答案是通过&lt;strong&gt;内容集合&lt;/strong&gt;。通过将 &lt;code&gt;src/content/posts&lt;/code&gt; 目录定义为内容集合，让 Astro 统一管理其中的内容，每篇博客文章都作为该集合中的一个条目。&lt;/p&gt;
&lt;p&gt;Astro 会自动扫描 &lt;code&gt;src/content&lt;/code&gt; 目录下的所有子目录，将每个子目录识别为一个独立的内容集合。通过在 &lt;code&gt;src/content&lt;/code&gt; 目录下添加 &lt;code&gt;config.ts&lt;/code&gt; 文件，可以定义 &lt;strong&gt;Schema&lt;/strong&gt; 来规范集合中内容的 Front Matter 或条目数据，这些 Schema 会通过 &lt;strong&gt;Zod&lt;/strong&gt; 进行数据验证，确保数据格式的一致性。&lt;/p&gt;
&lt;p&gt;项目中 &lt;code&gt;src/content/config.ts&lt;/code&gt; 文件内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineCollection, z } from &quot;astro:content&quot;;

const postsCollection = defineCollection({
	schema: z.object({
		title: z.string(),
		published: z.date(),
		updated: z.date().optional(),
		draft: z.boolean().optional().default(false),
		description: z.string().optional().default(&quot;&quot;),
		image: z.string().optional().default(&quot;&quot;),
		tags: z.array(z.string()).optional().default([]),
		category: z.string().optional().nullable().default(&quot;&quot;),
		lang: z.string().optional().default(&quot;&quot;),
		pinned: z.boolean().optional().default(false),
		author: z.string().optional().default(&quot;&quot;),
		sourceLink: z.string().optional().default(&quot;&quot;),
		licenseName: z.string().optional().default(&quot;&quot;),
		licenseUrl: z.string().optional().default(&quot;&quot;),

		/* Page encryption fields */
		encrypted: z.boolean().optional().default(false),
		password: z.string().optional().default(&quot;&quot;),

		series: z.string().optional(),

		/* For internal use */
		prevTitle: z.string().default(&quot;&quot;),
		prevSlug: z.string().default(&quot;&quot;),
		nextTitle: z.string().default(&quot;&quot;),
		nextSlug: z.string().default(&quot;&quot;),
	}),
});
const specCollection = defineCollection({
	schema: z.object({}),
});
export const collections = {
	posts: postsCollection,
	spec: specCollection,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astro 提供了辅助函数 &lt;code&gt;getCollection()&lt;/code&gt;，用于获取指定集合的全部内容，并以数组形式返回所有条目。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;src/pages/posts/[...slug].astro&lt;/code&gt; 文件中，通过调用了 &lt;code&gt;getSortedPosts()&lt;/code&gt; 函数间接使用了 &lt;code&gt;getCollection()&lt;/code&gt; 函数，最终获得了经过排序的博客文章数组。&lt;/p&gt;
&lt;p&gt;以下是第一篇博客文章的条目结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  id: &apos;astro-firefly-blog-deploy/part1.md&apos;,
  data: {
    title: &apos;基于 Firefly 主题的 Astro 博客部署（一）：从零到 GitHub Pages&apos;,
    published: 2025-10-29T00:00:00.000Z,
    draft: false,
    description: &apos;记录了自己是如何使用开源的 Firefly 主题，在 GitHub Pages 上从零开始部署基于 Astro 的个人博客。&apos;,
    image: &apos;&apos;,
    tags: [
      &apos;博客部署&apos;,
      &apos;Astro&apos;,
      &apos;Firefly&apos;,
      &apos;开源主题&apos;,
      &apos;GitHub Pages&apos;,
      &apos;GitHub Actions&apos;,
      &apos;Git&apos;
    ],
    category: &apos;实操记录&apos;,
    lang: &apos;&apos;,
    pinned: false,
    author: &apos;&apos;,
    sourceLink: &apos;&apos;,
    licenseName: &apos;&apos;,
    licenseUrl: &apos;&apos;,
    encrypted: false,
    password: &apos;&apos;,
    prevTitle: &apos;&apos;,
    prevSlug: &apos;&apos;,
    nextTitle: &apos;&apos;,
    nextSlug: &apos;&apos;
  },
  body: &apos;## 前言\r\n&apos; +
    &apos;\r\n&apos; +
    &apos;...&apos;,
  filePath: &apos;src/content/posts/astro-firefly-blog-deploy/part1.md&apos;,
  digest: &apos;474c5b5ac94b1108&apos;,
  rendered: {
    html: &apos;&amp;lt;section&amp;gt;&amp;lt;h2 id=&quot;前言&quot;&amp;gt;前言&amp;lt;a class=&quot;anchor&quot; href=&quot;#前言&quot;&amp;gt;....&apos;,
    metadata: {
      headings: [Array],
      localImagePaths: [],
      remoteImagePaths: [],
      frontmatter: [Object],
      imagePaths: []
    }
  },
  collection: &apos;posts&apos;,
  slug: &apos;astro-firefly-blog-deploy-part1&apos;,
  render: [Function: render]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察上面的条目可以发现，虽然文章文件路径为 &lt;code&gt;src/content/posts/astro-firefly-blog-deploy/part1.md&lt;/code&gt;，但其 &lt;code&gt;slug&lt;/code&gt; 字段的值为 &lt;code&gt;astro-firefly-blog-deploy-part1&lt;/code&gt;。值得注意的是，尽管在 &lt;code&gt;src/content/config.ts&lt;/code&gt; 文件中的 Scheme 中并未定义 &lt;code&gt;slug&lt;/code&gt; 字段，但仍然成功获取到了文章 Front Matter 中设置的 &lt;code&gt;slug&lt;/code&gt; 值。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;slug&lt;/code&gt; 字段与 &lt;code&gt;data&lt;/code&gt; 字段处于同一层级，猜测 Astro 会自动提取 Front Matter 中的 &lt;code&gt;slug&lt;/code&gt; 字段值。这点可以通过检查其他未设置 &lt;code&gt;slug&lt;/code&gt; 值的文章条目，观察其行为差异来验证。&lt;/p&gt;
&lt;p&gt;在动态路由的实现中，通过在组件脚本中导出 &lt;code&gt;getStaticPaths()&lt;/code&gt; 函数，其中该函数返回包含 &lt;code&gt;params&lt;/code&gt; 字段的对象数组，每个 &lt;code&gt;params&lt;/code&gt; 对象都包含一个 &lt;code&gt;slug&lt;/code&gt; 字段。这意味着，通过修改文章 Front Matter 中的 &lt;code&gt;slug&lt;/code&gt; 值，能够控制文章最终生成的访问路径。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
在撰写本文章的时候，虽然 Firefly 主题使用的 Astro 版本是 &lt;code&gt;5.15.3&lt;/code&gt;，但是它使用的内容集合 API 是旧版的。Astro 5 引入了内容层 API 用于替代旧的版本 API。例如，在 Astro 5 中，要定义集合，必须在项目中创建 &lt;code&gt;src/content.config.ts&lt;/code&gt; 文件，且在使用 &lt;code&gt;defineCollection()&lt;/code&gt; 函数的时候必须配置 &lt;code&gt;loader&lt;/code&gt; 用于加载数据源。再比如，渲染方法 &lt;code&gt;entry.render()&lt;/code&gt; 被 &lt;code&gt;render(entry)&lt;/code&gt; 函数取代。如果在项目里 &lt;code&gt;src/content/config.ts&lt;/code&gt; 文件中强行添加 &lt;code&gt;loader: glob({ ... })&lt;/code&gt;，也就启用了 Astro 5 的新内容层 API，这导致项目里 &lt;code&gt;entry.render()&lt;/code&gt; 调用失败。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;变量注入&lt;/h4&gt;
&lt;p&gt;根据前面的分析，文章条目对象的 &lt;code&gt;data&lt;/code&gt; 字段中存储着文章元数据。&lt;/p&gt;
&lt;p&gt;组件脚本的另一个重要作用是创建可在模板中使用的变量。这些变量通过 Astro 的组件模板语法在组件模板中被引用。&lt;/p&gt;
&lt;p&gt;具体实现的过程如下：将文章条目对象通过 &lt;code&gt;getStaticPaths()&lt;/code&gt; 函数返回对象的 &lt;code&gt;props&lt;/code&gt; 字段传入后，可以在组件脚本里通过 &lt;code&gt;Astro.props&lt;/code&gt; 获取该对象，并基于其声明变量。随后，可以使用类似 JSX 语法在组件模板中使用。&lt;/p&gt;
&lt;p&gt;通过在组件模板中使用组件脚本中定义的数据和值，最终生成动态创建的 HTML：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
export async function getStaticPaths() {
  const blogEntries = await getSortedPosts();
  return blogEntries.map((entry) =&amp;gt; ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}

const { entry } = Astro.props;
// ...

---
&amp;lt;MainGridLayout
  banner={entry.data.image}
  title={entry.data.title}
  description={entry.data.description}
  lang={entry.data.lang}
  setOGTypeArticle={true}
  postSlug={entry.slug}
  headings={headings}
&amp;gt;
&amp;lt;!-- ... --&amp;gt;
&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>基于 Firefly 主题的 Astro 博客部署（一）：从零到 GitHub Pages</title><link>https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part1/</link><guid isPermaLink="true">https://xpfxzxc.github.io/posts/astro-firefly-blog-deploy-part1/</guid><description>记录了自己是如何使用开源的 Firefly 主题，在 GitHub Pages 上从零开始部署基于 Astro 的个人博客。</description><pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前几天，我像平时一样在网站 &lt;a href=&quot;https://linux.do/&quot;&gt;&lt;strong&gt;LINUX DO&lt;/strong&gt;&lt;/a&gt; 上逛帖子，偶然看到有人开源了一个名为 &lt;strong&gt;Firefly&lt;/strong&gt; 的博客主题模板。在查看了主题的预览效果图并访问了演示站点后，我认为它设计精致、外观美观，且集成了众多的实用功能。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;CuteLeaf/Firefly&quot;}&lt;/p&gt;
&lt;h3&gt;回顾&lt;/h3&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;曾经有过搭建个人博客的想法，但一直未付诸实践，甚至觉得没必要&lt;/li&gt;
&lt;li&gt;今年频繁与 AI 交流，许多问题反复提问，相关知识点也被反复提及，但我仍未记住；相关问题和回答分散到各网站上，不方便去检索&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;需求&lt;/h3&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;无需支付服务器和域名的费用&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;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;平台选择&lt;/h2&gt;
&lt;p&gt;根据需求，与 AI 沟通并查阅相关资料后，我觉得可以借助该开源项目在 &lt;strong&gt;GitHub Pages&lt;/strong&gt; 上部署个人博客网站。&lt;/p&gt;
&lt;p&gt;我进行了以下几方面的考虑，最终选择将 GitHub Pages 作为博客托管平台。&lt;/p&gt;
&lt;h3&gt;完全免费&lt;/h3&gt;
&lt;p&gt;GitHub Pages 对公开仓库免费提供静态网站托管服务，无需支付服务器或流量费用。我不用再操心服务器域名的费用，同时也让网站启用了 &lt;strong&gt;HTTPS&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;长期稳定&lt;/h3&gt;
&lt;p&gt;近几年，伴随大语言模型的冲击，问答社区、博客等网站，比如：&lt;strong&gt;CSDN&lt;/strong&gt;、&lt;strong&gt;简书&lt;/strong&gt;、&lt;strong&gt;StackOverflow&lt;/strong&gt; 等网站迅速走向衰弱。然而，&lt;strong&gt;GitHub&lt;/strong&gt; 作为代码托管平台反而越来越火。从长远来看，GitHub 会倒闭的可能性极低。&lt;/p&gt;
&lt;h3&gt;与 GitHub 无缝集成&lt;/h3&gt;
&lt;p&gt;博客网站、博客内容以文件以代码形式存在，天然支持版本控制。借助 GitHub 页面 UI 和 &lt;strong&gt;Git&lt;/strong&gt;，可以很好地处理好各方的远程仓库和本地仓库的版本控制。GitHub 自带的 &lt;strong&gt;Discussion&lt;/strong&gt; 又能免费地作为博客文章的讨论区。&lt;/p&gt;
&lt;h3&gt;部署简单，可自动化&lt;/h3&gt;
&lt;p&gt;只需将静态文件推送到远程仓库指定的分支，GitHub 就会自动构建并发布。更进一步，允许借助 &lt;strong&gt;GitHub Actions&lt;/strong&gt; 自定义自动化流程。&lt;/p&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;我打算先让博客上线运行，之后再逐步调整。&lt;/p&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;作为博客网站，最重要的是展示博客文章内容。博客文章内容相对稳定，除非博主更新文章，不然用户反复刷新网页，网页的内容也不会变化。用户通常是通过点击链接，从一个页面跳转到另一个页面，几乎不需要再进行额外的复杂交互。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://astro.build/&quot;&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/a&gt; 作为静态站点生成器，构建时生成的 &lt;strong&gt;HTML&lt;/strong&gt; 文件可直接放到免费的托管平台上运行，不需要服务器。生成的网页不带多余的 &lt;strong&gt;JavaScript&lt;/strong&gt;（可以按需加载），加载速度飞快，对 &lt;strong&gt;SEO&lt;/strong&gt; 友好。由此可见，Astro 很适配我的需求。&lt;/p&gt;
&lt;p&gt;关于仓库的版本控制和管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我想能轻松拉取原 Firefly 代码的更改，所以需要先 &lt;code&gt;fork&lt;/code&gt; 一份到我的仓库。&lt;/li&gt;
&lt;li&gt;我想能定制化代码、发表博客文章，所以需要 &lt;code&gt;clone&lt;/code&gt; 到本地仓库。&lt;/li&gt;
&lt;li&gt;我想能选择性地获取原 Firefly 代码的更新、定制化代码、发表博客内容，所以需要新建分支 &lt;code&gt;custom&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;我想以后能贡献代码给原 Firefly，所以不能改动主分支 &lt;code&gt;master&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;我想将 &lt;code&gt;https://&amp;lt;username&amp;gt;.github.io&lt;/code&gt; 作为我的博客主页，所以我需要新建仓库 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;（将 &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; 替换为 GitHub 用户名，下文同理）。&lt;/li&gt;
&lt;li&gt;我想让我对博客网站的更改，能自动被推送到线上，所以我需要在我的 Fork 仓库配置一个 GitHub Actions 工作流：每当向我的 Fork 仓库推送代码时，GitHub Actions 会自动构建静态网站，并将生成的文件推送到 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; 仓库，从而更新线上站点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;思路理清了后，就开始动手实操了。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;h4&gt;1. 创建仓库 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;在创建仓库页面中，依次填入以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Repository name&lt;/code&gt;：&lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Description&lt;/code&gt;：&lt;code&gt;Personal blog website hosted on GitHub Pages&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接点击 &lt;code&gt;Create Repository&lt;/code&gt; 按钮创建仓库，无需调整其他选项。&lt;/p&gt;
&lt;h4&gt;2. &lt;code&gt;Fork&lt;/code&gt; 原项目到我的仓库&lt;/h4&gt;
&lt;p&gt;打开项目 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot;&gt;&lt;strong&gt;Firefly&lt;/strong&gt;&lt;/a&gt; 主页，点击页面右上角的 &lt;code&gt;Fork&lt;/code&gt; 按钮，无需修改任何选项，直接点击 &lt;code&gt;Create fork&lt;/code&gt; 按钮即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
原项目使用 &lt;code&gt;master&lt;/code&gt; 作为默认分支，而非现在常见的 &lt;code&gt;main&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. 将我的 Fork 仓库 &lt;code&gt;clone&lt;/code&gt; 到本地&lt;/h4&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:&amp;lt;username&amp;gt;/Firefly.git
code Firefly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将用 &lt;strong&gt;VSCode&lt;/strong&gt; 打开刚拉取下来的项目。&lt;/p&gt;
&lt;h4&gt;4. 在本地仓库创建并切换到 &lt;code&gt;custom&lt;/code&gt; 分支&lt;/h4&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch custom
git checkout custom
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 禁用会报错的 workflow 文件&lt;/h4&gt;
&lt;p&gt;在实操过程中，我发现 &lt;code&gt;.github/workflows&lt;/code&gt; 目录下工作流文件 &lt;code&gt;biome.yml&lt;/code&gt; 和 &lt;code&gt;build.yml&lt;/code&gt; 运行时会报错。考虑到后续可能需要修改，暂不删除，而是通过在文件名末尾添加 &lt;code&gt;.disabled&lt;/code&gt; 后缀来禁用它们。&lt;/p&gt;
&lt;h4&gt;6. 修改 workflow 文件以自动部署到另一个仓库&lt;/h4&gt;
&lt;p&gt;根据之前的思路：每当接收到推送事件时，自动构建静态网站，并将生成的文件推送到 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; 仓库。&lt;/p&gt;
&lt;p&gt;需要修改 &lt;code&gt;.github/workflows&lt;/code&gt; 目录下的 &lt;code&gt;deploy.yml&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Deploy to Pages Branch

on:
-  # 每次推送到 `main` 分支时触发这个&quot;工作流程&quot;
-  # 如果你使用了别的分支名，请按需将 `main` 替换成你的分支名
+  # 每次推送到 `custom` 分支时触发这个&quot;工作流程&quot;
  push:
-    branches: [ main ]
+    branches: [ custom ]
  # 允许你在 GitHub 上的 Actions 标签中手动触发此&quot;工作流程&quot;
  workflow_dispatch:

# 需要写入权限来推送到pages分支
permissions:
  contents: write

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: &apos;20&apos;
          
      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9.14.4
          run_install: false
          
      - name: Install dependencies
        run: pnpm install --no-frozen-lockfile
      
      - name: Build site
        run: pnpm run build
      
      - name: Deploy to pages branch
        uses: JamesIves/github-pages-deploy-action@v4
        with:
-          branch: pages # 部署到pages分支
+          token: ${{ secrets.GH_PAT }} # 需要在仓库的Settings-&amp;gt;Secrets and variables-&amp;gt;Actions中添加GH_PAT，该Token需要有写入权限
+          repository-name: &amp;lt;username&amp;gt;/&amp;lt;username&amp;gt;.github.io # 你的GitHub Pages仓库名称
+          branch: main # 部署到custom分支
          folder: dist # Astro默认构建输出目录
          clean: true # 清理目标分支中的旧文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7. 创建 &lt;strong&gt;PAT&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;点击 GitHub 账号右上角头像 → 选择 &lt;code&gt;Settings&lt;/code&gt; → 左侧边栏点击 &lt;code&gt;Developer settings&lt;/code&gt; → 点击 &lt;code&gt;Personal access tokens&lt;/code&gt; → 选择 &lt;code&gt;Fine-grained tokens&lt;/code&gt; → 点击右侧 &lt;code&gt;Generate new token&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在创建页面中，依次填写以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Token name&lt;/code&gt;： 为令牌命名（如：&lt;code&gt;blog-deploy&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expiration&lt;/code&gt;： 设置过期时间（如：&lt;code&gt;90 days&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Repository access&lt;/code&gt;：选择 &lt;code&gt;Only select repositories&lt;/code&gt;，并仅勾选 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Permissions&lt;/code&gt;：点击 &lt;code&gt;Add permissions&lt;/code&gt;，添加以下三项权限：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Contents&lt;/code&gt; → 设为 &lt;code&gt;Read and write&lt;/code&gt;（用于更改仓库内容）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Pages&lt;/code&gt; → 设为 &lt;code&gt;Read and write&lt;/code&gt;（用于部署 GitHub Pages）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Metadata&lt;/code&gt; → 保持默认（&lt;code&gt;Read-only&lt;/code&gt;，自动包含）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样可避免生成的令牌可访问的仓库范围过广或权限过大。&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;Generate token&lt;/code&gt; 按钮，并记录下生成的令牌。&lt;/p&gt;
&lt;h4&gt;8. 创建环境变量&lt;/h4&gt;
&lt;p&gt;在我的 Fork 仓库页面，点击顶部导航栏的 &lt;code&gt;Settings&lt;/code&gt; → 左侧边栏点击 &lt;code&gt;Secrets and variables&lt;/code&gt; → 选择下拉项 &lt;code&gt;Actions&lt;/code&gt; → 点击右侧 &lt;code&gt;New repository secret&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;p&gt;在创建页面中，依次填写以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Name&lt;/code&gt;：&lt;code&gt;GH_PAT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Secret&lt;/code&gt;：粘贴刚才保存的令牌&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;点击 &lt;code&gt;Add secret&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;h4&gt;9. 修改部署地址&lt;/h4&gt;
&lt;p&gt;根据 Astro 官方文档里的&lt;a href=&quot;https://docs.astro.build/en/guides/deploy/github/&quot;&gt;部署指南&lt;/a&gt;，需要将 &lt;code&gt;astro.config.mjs&lt;/code&gt; 文件里的 &lt;code&gt;site&lt;/code&gt; 改成 GitHub Pages 部署地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig({
-    site: &quot;https://demo-firefly.netlify.app/&quot;,
+    site: &quot;https://&amp;lt;username&amp;gt;.github.io&quot;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;10. 添加 &lt;code&gt;.nojekyll&lt;/code&gt; 文件&lt;/h4&gt;
&lt;p&gt;在使用 Astro 构建项目时，Astro 会在 &lt;code&gt;dist&lt;/code&gt; 输出目录中生成一个 &lt;code&gt;_astro&lt;/code&gt; 文件夹，其中包含 JavaScript 代码和静态资源。&lt;/p&gt;
&lt;p&gt;GitHub Pages 对于基于分支自动构建的方式，会默认启用 &lt;strong&gt;Jekyll&lt;/strong&gt; 构建引擎，而 Jekyll 会自动忽略所有以下划线 &lt;code&gt;_&lt;/code&gt; 开头的文件和目录。&lt;/p&gt;
&lt;p&gt;因此，当基于分支把 Astro 构建出的 &lt;code&gt;dist&lt;/code&gt; 目录部署到 GitHub Pages 时，&lt;code&gt;_astro&lt;/code&gt; 目录会被 Jekyll 忽略，不会被发布，这导致网页显示不正常，功能异常。&lt;/p&gt;
&lt;p&gt;解决办法很简单，在部署目录 &lt;code&gt;dist&lt;/code&gt; 添加一个名为 &lt;code&gt;.nojekyll&lt;/code&gt; 的空文件，用于告诉 GitHub Pages：不要用 Jekyll 处理这个站点。&lt;/p&gt;
&lt;p&gt;实际上，在我的项目 &lt;code&gt;public&lt;/code&gt; 目录下添加空文件 &lt;code&gt;.nojekyll&lt;/code&gt; 即可。因为 Astro 在构建站点时，会将它复制到 &lt;code&gt;dist&lt;/code&gt; 部署目录。&lt;/p&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch public/.nojekyll
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;11. 提交 commit 并推送代码到远程仓库&lt;/h4&gt;
&lt;p&gt;在项目目录下运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rm .github/workflows/biome.yml
git add .github/workflows/biome.yml.disabled
git rm .github/workflows/build.yml
git add .github/workflows/build.yml.disabled
git commit -m &quot;chore: 临时禁用冗余的 workflow 文件&quot;

git add .github/workflows/deploy.yml
git commit -m &quot;chore: 调整工作流文件中的引用 main 为 custom，并使打包后的文件部署到另一个仓库的 main 分支上&quot;

git add astro.config.mjs
git commit -m &quot;修改部署地址&quot;

git add public/.nojekyll
git commit -m &quot;fix: 添加文件 public/.nojekyll 以正确提供 _astro 目录中的文件&quot;

git push origin custom
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;12. 验证部署结果&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;打开我的 Fork 仓库页面，点击顶部导航栏的 &lt;code&gt;Actions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;等待最新的工作流运行完成，确保没有发生错误&lt;/li&gt;
&lt;li&gt;打开 &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; 仓库页面，可看到已部署好的站点文件。GitHub Pages 会自动从 &lt;code&gt;main&lt;/code&gt; 分支部署站点，无需额外操作&lt;/li&gt;
&lt;li&gt;点击顶部导航栏的 &lt;code&gt;Actions&lt;/code&gt;，等待 GitHub Pages 部署站点完成&lt;/li&gt;
&lt;li&gt;访问 &lt;code&gt;https://&amp;lt;username&amp;gt;.github.io&lt;/code&gt;，观察页面是否正常显示&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;F12&lt;/code&gt; 键打开&lt;code&gt;开发人员工具&lt;/code&gt;，切换到&lt;code&gt;控制台&lt;/code&gt;面板，检查其中是否有严重错误报告信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;后续&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;当上游更新代码后，可在我的 Fork 仓库页面点击 &lt;code&gt;Sync fork&lt;/code&gt; 按钮同步更新&lt;/li&gt;
&lt;li&gt;建议在 &lt;code&gt;custom&lt;/code&gt; 分支上进行个性化定制（如撰写博客文章）&lt;/li&gt;
&lt;li&gt;当本地仓库需要同步上游更新时，有以下方式：
&lt;ul&gt;
&lt;li&gt;完整同步：在 &lt;code&gt;master&lt;/code&gt; 分支上拉取上游最新更改，再通过 &lt;code&gt;git merge master&lt;/code&gt; 或 &lt;code&gt;git rebase master&lt;/code&gt; 将全部变更整合到 &lt;code&gt;custom&lt;/code&gt; 分支&lt;/li&gt;
&lt;li&gt;部分同步：使用 &lt;code&gt;git cherry-pick&lt;/code&gt; 选取特定提交，或通过 &lt;code&gt;git checkout master -- &amp;lt;file&amp;gt;&lt;/code&gt; 仅更新个别文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当想为原主题仓库贡献代码时，可先在 &lt;code&gt;master&lt;/code&gt; 主分支上拉取上游最新更改。然后，基于主分支创建新的分支，并根据工作性质合理命名，例如新功能使用 &lt;code&gt;feature/xxx&lt;/code&gt;，Bug 修复使用 &lt;code&gt;fix/xxx&lt;/code&gt;。在新分支上完成修改并提交后，将其推送到自己的 Fork 仓库。最后向原主题仓库发起 &lt;strong&gt;Pull Request&lt;/strong&gt; 请求合并分支。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>