Astro + Fuwari 博客搭建:从主题到个人化改造
搭个人博客最容易卡住的地方不是「能不能跑起来」,而是跑起来之后不像自己的东西——首页还是模板味,文章卡片没有记忆点,详情页信息太杂,评论和音乐播放器又散落在各种教程里。
这篇按我当前博客的实际结构写:先用 Astro + Fuwari 搭出基础,再逐步接入音乐播放器、Giscus 评论、首页 Hero 改造、文章列表卡片、文章详情页布局和部署检查。目标不是复刻原主题,而是把它改成适合长期写技术笔记的个人站。
一、整体方案#
当前博客的核心是:
| 模块 | 选择 | 作用 |
|---|---|---|
| 静态框架 | Astro | 生成静态页面,适合部署到 EdgeOne Pages、Cloudflare Pages、Vercel |
| 主题基础 | Fuwari | 提供文章系统、归档、RSS、Markdown 渲染、目录、明暗主题 |
| 样式体系 | Tailwind CSS + Stylus | 快速写布局,统一主题变量 |
| 页面切换 | Swup | 页面过渡和局部更新 |
| 评论 | Giscus | 使用 GitHub Discussions 做评论区 |
| 音乐 | APlayer + Meting | 底部悬浮播放器 |
| 搜索 | Pagefind | 构建后生成本地搜索索引 |
为什么不从零写主题?
| 方案 | 优点 | 问题 |
|---|---|---|
| 从零写 Astro 博客 | 结构完全可控 | 文章集合、RSS、分页、目录、Markdown 插件都要自己补 |
| 直接用 Fuwari | 功能完整 | 首页和文章页模板味比较重 |
| 在 Fuwari 上改 | 保留成熟功能,同时改视觉 | 需要读懂布局和组件之间的关系 |
这里走第三种:保留 Fuwari 的内容系统,重写关键视觉层。
二、初始化 Astro + Fuwari#
如果是新项目,可以直接用 Fuwari 模板初始化;如果已经有仓库,就拉下来安装依赖。
1pnpm install本地启动:
1pnpm run dev构建:
1pnpm run build预览构建结果:
1pnpm run preview当前项目的核心命令在 package.json:
1{2 "scripts": {3 "dev": "astro dev",4 "check": "astro check",5 "build": "astro build && pagefind --site dist",6 "preview": "astro preview",7 "new-post": "node scripts/new-post.js",8 "format": "biome format --write ./src",9 "lint": "biome check --write ./src"10 }11}构建命令后面接了 pagefind --site dist,意思是 Astro 先生成静态站点,再给 dist 目录建立搜索索引。
三、先改全站配置#
站点配置按职责拆分在:
1src/config/site.ts2src/config/navigation.ts3src/config/integrations.tssite.ts 管站点标题、语言、主题色、头像和社交链接:
1export const siteConfig = {2 title: "weidas Blog",3 subtitle: "在折腾中生活,在探索中成长",4 lang: "zh_CN",5 themeColor: {6 hue: 105,7 fixed: false,8 },9};hue 是主题色相,范围是 0-360。例如:
| 色相 | 大致颜色 |
|---|---|
0 | 红色 |
60 | 黄色 |
105 | 偏绿色 |
200 | 青色 |
240 | 蓝色 |
270 | 紫色 |
330 | 粉色 |
导航在 src/config/navigation.ts:
1export const navBarConfig = {2 links: [3 LinkPreset.Home,4 LinkPreset.Archive,5 LinkPreset.About,6 {7 name: "常用工具",8 url: "https://webtools.example.com/",9 external: true,10 icon: "fa6-solid:screwdriver-wrench",11 },12 ],13};只改导航文字、链接、图标时,优先改 src/config/navigation.ts。只有要改 Dock 外观时,才去动 src/components/Navbar.astro。
四、整理目录结构#
日常真正高频修改的是这些位置:
| 路径 | 作用 |
|---|---|
src/content/posts/ | 博客文章 |
src/content/spec/about.md | 关于页 |
src/config/ | 站点、导航和第三方集成配置 |
src/assets/images/avatar.jpeg | 头像 |
src/assets/images/hero-carousel/ | 首页轮播背景 |
src/assets/images/posts/ | 文章封面 |
src/components/HomeHero.astro | 首页 Hero |
src/components/post/PostCard.astro | 首页文章卡片 |
src/pages/posts/[...slug].astro | 文章详情页 |
src/components/integrations/Giscus.astro | 评论系统 |
src/components/integrations/MusicPlayer.astro | 音乐播放器 |
src/layouts/MainGridLayout.astro | 全站布局骨架 |
src/styles/markdown.css | Markdown 正文样式 |
src/styles/variables.styl | 全站颜色变量 |
生成目录不要手动改:
1dist/2.astro/3node_modules/这些目录会由构建工具生成。手动改了也不会成为真正的源码。
五、写文章和封面#
文章放在:
1src/content/posts/可以用脚本新建:
1pnpm run new-post my-note --title "我的笔记"也可以直接写 Markdown。Frontmatter 示例:
1---2title: Astro + Fuwari 博客搭建:从主题到个人化改造3published: 2026-03-184description: 记录从 Astro + Fuwari 初始化个人博客,到接入音乐播放器、Giscus 评论、首页 Hero、文章卡片、详情页布局、部署检查的完整改造流程。5image: ../../assets/images/posts/astro-fuwari-blog-guide.webp6tags: ["Astro", "Fuwari", "博客", "Giscus", "APlayer", "前端"]7category: 博客与写作8---封面图建议放:
1src/assets/images/posts/从 src/content/posts/<slug>.md 引用时,路径是:
1image: ../../assets/images/posts/<slug>.webp首页文章卡片使用 16:9 比例,推荐封面尺寸:
| 尺寸 | 说明 |
|---|---|
1280x720 | 最低建议 |
1600x900 | 推荐 |
1920x1080 | 更清晰,适合后续复用 |
图片会使用 object-fit: cover 裁切,所以重要内容不要贴边。
六、接入音乐播放器#
音乐播放器组件在:
1src/components/integrations/MusicPlayer.astro当前使用 APlayer + Meting:
1<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css">2<script is:inline src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>3<script is:inline src="https://cdn.jsdelivr.net/npm/meting@2/dist/Meting.min.js"></script>4
5<meting-js6 server="netease"7 type="playlist"8 id="17997617137"9 fixed="true"10 mini="true"11 order="random"12 loop="all"13 volume="0.7"14 lrc-type="1">15</meting-js>常改参数:
| 参数 | 作用 | 示例 |
|---|---|---|
server | 音乐平台 | netease |
type | 类型 | playlist、song、album |
id | 歌单或歌曲 ID | 17997617137 |
fixed | 固定在页面底部 | true |
mini | 迷你模式 | true |
order | 播放顺序 | random |
loop | 循环方式 | all |
volume | 默认音量 | 0.7 |
组件在全站布局里挂载:
1<MusicPlayer />全站挂载位置在:
1src/components/layout/SiteShell.astro这样所有页面都会显示播放器,不需要每个页面单独引入。
七、接入 Giscus 评论#
评论组件在:
1src/components/integrations/Giscus.astro配置集中成一个对象:
1const giscusConfig = {2 repo: "weidas/Giscus-weidas-blog",3 repoId: "你的 Giscus repoId",4 category: "Announcements",5 categoryId: "你的 Giscus categoryId",6 mapping: "pathname",7 strict: "0",8 reactionsEnabled: "1",9 emitMetadata: "0",10 inputPosition: "bottom",11 theme: "preferred_color_scheme",12 lang: "zh-CN",13};Giscus 的核心是 GitHub Discussions:
| 字段 | 说明 |
|---|---|
repo | 用来存评论的 GitHub 仓库 |
repoId | 仓库 ID |
category | Discussions 分类名 |
categoryId | 分类 ID |
mapping | 页面和讨论的映射方式 |
theme | 评论区主题 |
lang | 评论区语言 |
文章详情页里直接引入:
1import Giscus from "@components/integrations/Giscus.astro";2
3<Giscus />当前项目把评论放在正文后面,路径是:
1src/pages/posts/[...slug].astro这样每篇文章都有独立评论区,映射方式用 pathname,URL 不变时评论就不会丢。
八、改首页 Hero#
首页 Hero 组件在:
1src/components/HomeHero.astro它做了几件事:
| 功能 | 实现位置 |
|---|---|
| 背景轮播 | getHeroImages() 从统一图片清单读取 hero-carousel |
| 花瓣飘落 | .petal-layer 和 @keyframes petal-fall |
| 圆形头像 | avatarImage |
| 打字机文案 | .typewriter |
| GitHub 链接 | profileConfig.links |
| 个人标签 | personalTags |
轮播图目录:
1src/assets/images/hero-carousel/读取方式:
1const imageModules = import.meta.glob<{ default: ImageMetadata }>(2 "../../assets/images/hero-carousel/*.{jpg,jpeg,png,webp,avif}",3 {4 eager: true,5 },6);轮播切换时间:
1const slideDuration = 60;打字机文字:
1const emoText = "有些人像黄昏,来时温柔,去时荒凉";个人标签:
1const personalTags = [2 { label: "VPS 驯兽师", href: "/archive/" },3 { label: "逆向爱好者", href: "/archive/" },4 { label: "服务器炼金术士", href: "/archive/" },5 { label: "NixOS 信徒", href: "/archive/" },6 { label: "Root 玩家", href: "/archive/" },7 { label: "终端效率洁癖", href: "/archive/" },8];这里的设计原则是:首页只负责建立气质,不负责塞满信息。文章列表往下滚就能看到,Hero 区只保留头像、文案、个人入口和几个关键词。
九、改文章卡片#
首页文章卡片在:
1src/components/post/PostCard.astro当前卡片结构是:
1<article class:list={["note-card", className]} style={style}>2 <a href={url} class="note-link" aria-label={title}>3 <div class="note-cover-wrap">4 <ImageWrapper class="note-cover" />5 </div>6
7 <div class="note-body">8 <h2>{title}</h2>9 <p>{description}</p>10 <div class="note-meta-row">11 <time>{published}</time>12 <span class="word-count">字数</span>13 <span class="note-tags">标签</span>14 </div>15 </div>16 </a>17</article>封面比例由 CSS 控制:
1.note-cover-wrap {2 width: 100%;3 aspect-ratio: 16 / 9;4 overflow: hidden;5 background: #07080d;6}7
8.note-cover img {9 width: 100%;10 height: 100%;11 object-fit: cover;12}卡片只保留这些信息:
| 信息 | 保留原因 |
|---|---|
| 封面 | 建立视觉记忆 |
| 标题 | 文章入口 |
| 摘要 | 帮读者判断内容 |
| 日期 | 判断新旧 |
| 字数 | 判断阅读成本 |
| 标签 | 判断主题 |
分类没有放在卡片里,因为标签已经足够表达主题。分类适合归档统计,不适合每张卡片都展示。
十、改文章详情页#
文章详情页在:
1src/pages/posts/[...slug].astro当前顶部只保留:
| 元素 | 说明 |
|---|---|
| 标题 | 页面核心 |
| 头像 | 作者识别 |
| 作者名 | 个人博客标识 |
| 日期 | 发布时间 |
| 标签 | 当前文章主题 |
结构大致是:
1<header class="post-header">2 <h1>{entry.data.title}</h1>3
4 <div class="post-author-line">5 <img src={avatarImage.src} alt={profileConfig.name} />6 <span>{profileConfig.name}</span>7 <time>{formatDateToYYYYMMDD(entry.data.published)}</time>8 <span class="post-tags">9 {entry.data.tags.map((tag) => <span class="post-tag">#{tag}</span>)}10 </span>11 </div>12</header>13
14<Markdown>15 <Content />16</Markdown>17
18<Giscus />删掉了这些默认信息:
| 被删内容 | 原因 |
|---|---|
| PV 次数 | 当前没有稳定统计系统,先不放 |
| 文章地址复制块 | 对个人博客正文干扰大 |
| 作者信息大卡片 | 顶部头像已经够了 |
| 许可协议块 | 页脚和站点说明已经覆盖 |
正文宽度由 MainGridLayout.astro 控制:
1isPostPage2 ? "grid grid-cols-1 xl:grid-cols-[1fr_minmax(0,64rem)_16rem] 2xl:grid-cols-[1fr_minmax(0,70rem)_17rem]"3 : "grid grid-cols-1"也就是:
1左侧空白 / 中间正文 / 右侧目录中间正文最大宽度在大屏下是 64rem 到 70rem,比原主题更宽,适合长教程。
十一、保留右侧目录#
目录逻辑在:
1src/layouts/MainGridLayout.astro2src/components/widget/TOC.astro只在文章页显示:
1{siteConfig.toc.enable && isPostPage && (2 <div id="toc-wrapper">3 <TOC headings={headings}></TOC>4 </div>5)}开关在 src/config/site.ts:
1toc: {2 enable: true,3 depth: 3,4}长教程必须保留目录。技术文章动不动几千字,没有目录就很难回到某个小节。
十二、改全站布局#
布局核心在:
1src/layouts/MainGridLayout.astro当前首页和文章页走不同布局:
| 页面 | 布局 |
|---|---|
| 首页 | 单列内容,顶部 Hero |
| 文章页 | 中间正文 + 右侧目录 |
| 关于页 / 归档页 | 普通内容布局 |
判断文章页:
1const isPostPage = Astro.url.pathname.startsWith(url("/posts/"));首页是否显示 Hero:
1const isHomePage = pathsEqual(Astro.url.pathname, url("/"));2const showHomeBanner = siteConfig.banner.enable && isHomePage;页面最大宽度在:
1src/constants/constants.ts1export const PAGE_WIDTH = 96;首页每页文章数量也在这里:
1export const PAGE_SIZE = 8;这些属于「常改但不要乱改」的参数。宽度改太大,正文阅读会散;每页文章太多,首页加载和浏览节奏都会变差。
十三、接入部署检查#
每次改完先跑:
1pnpm verifyEdgeOne Pages 这类静态托管平台通常填:
1构建命令:pnpm run build2输出目录:dist3包管理器:pnpm推送流程:
1git status2git add src/content/posts/my-note.md src/assets/images/posts/my-note.webp3git diff --cached4git commit -m "更新博客"5git push如果平台已经绑定仓库,git push 后会自动构建。
十四、常见故障速查#
| 现象 | 原因 | 处理 |
|---|---|---|
| 首页封面不显示 | image 路径写错 | 从文章文件位置计算相对路径 |
| 本地正常,构建失败 | Astro 动态图片没找到 | 检查 src/assets/images/posts/<slug>.webp 是否存在 |
| Giscus 不显示 | 仓库或分类 ID 错 | 重新到 giscus.app 生成配置 |
| 评论每篇文章串在一起 | mapping 配错 | 个人博客建议用 pathname |
| 音乐播放器没出现 | CDN 被拦或脚本未加载 | 检查浏览器 Network 和 Console |
| TOC 点击不跳转 | 标题 slug 或 Swup 容器问题 | 检查 rehype-slug 和 #toc 容器 |
| 构建后搜索无结果 | Pagefind 没跑 | 确认 build 命令包含 pagefind --site dist |
构建时看到这些不一定是错误:
1Pagefind doesn't support stemming for the language zh-cn.意思是中文没有英文那种词根匹配,搜索仍然可用。
1Browserslist: browsers data (caniuse-lite) is old.意思是浏览器兼容性数据库旧了,不会直接导致构建失败。
十五、几条实用经验#
- 先改配置,再改组件:标题、导航、头像、链接都在
src/config/,不要一开始就钻组件。 - 首页只负责气质:轮播图、头像、emo 文案、个人标签够了,不要把所有功能都塞进首屏。
- 文章卡片不要展示太多字段:标题、摘要、日期、字数、标签已经足够。
- 详情页顶部要克制:技术教程读者是来看内容的,不是看一堆元信息。
- 评论单独做组件:以后从 Giscus 换到别的系统,只改
Giscus.astro和详情页引用。 - 音乐播放器全站挂载:放在
Layout.astro,不要每个页面重复引。 - 所有常改点加标记:代码里用
★ 常改标出来,几个月后回来还能快速找到入口。 - 写新教程前先看规范:
src/content/posts/tutorial-style-guide.md是文章风格基准,标题、表格、代码块、结尾都按它来。