2571 字
13 分钟
Fuwari 博客主题深度架构解析:从源码到自定义开发

引言#

Fuwari 是一个基于 Astro 构建的静态博客主题,以其精美的设计、流畅的动画效果和丰富的功能受到众多博主的喜爱。本文将从架构层面深入剖析 Fuwari 的实现原理,帮助你理解其设计思想,并掌握自定义开发的方法。

一、整体架构概览#

1.1 技术栈组成#

Fuwari 采用了现代化的前端技术栈:

层级技术用途
框架Astro 5.x静态站点生成、 Islands 架构
样式Tailwind CSS 3.x原子化 CSS 样式
组件Svelte 5.x交互式组件(主题切换等)
动画Swup + CSS页面过渡动画
搜索Pagefind静态搜索索引
图标Iconify图标系统
代码高亮Expressive Code代码块渲染

1.2 项目结构#

fuwari/
├── src/
│ ├── components/ # 可复用组件
│ │ ├── control/ # 控制组件(分页、返回顶部)
│ │ ├── misc/ # 杂项组件(Markdown、图片)
│ │ └── widget/ # 侧边栏组件
│ ├── content/ # 内容集合配置
│ │ ├── config.ts # 文章 schema 定义
│ │ └── posts/ # 博客文章目录
│ ├── i18n/ # 国际化
│ ├── layouts/ # 页面布局
│ ├── pages/ # 路由页面
│ ├── plugins/ # 自定义 remark/rehype 插件
│ ├── styles/ # 全局样式
│ ├── types/ # TypeScript 类型定义
│ └── utils/ # 工具函数
├── public/ # 静态资源
└── scripts/ # 构建脚本

二、核心架构设计#

2.1 布局系统(Layout System)#

Fuwari 采用了三层布局架构

2.1.1 Layout.astro - 基础布局层#

这是所有页面的根布局,负责:

  • HTML 骨架和元数据
  • 主题色和暗色模式初始化
  • 全局样式变量注入
  • PhotoSwipe 图片灯箱初始化
  • Swup 页面过渡配置
<!-- 关键代码片段:主题初始化 -->
<script is:inline>
// 从 localStorage 读取主题设置
const theme = localStorage.getItem('theme') || DEFAULT_THEME;
switch (theme) {
case LIGHT_MODE: document.documentElement.classList.remove('dark'); break;
case DARK_MODE: document.documentElement.classList.add('dark'); break;
case AUTO_MODE: // 根据系统偏好自动切换
}
// 读取主题色相
const hue = localStorage.getItem('hue') || configHue;
document.documentElement.style.setProperty('--hue', hue);
</script>

2.1.2 MainGridLayout.astro - 主网格布局层#

这是博客页面的主要布局,实现了响应式三栏设计

┌─────────────────────────────────────────────┐
│ Navbar │
├─────────────────────────────────────────────┤
│ │
│ Banner │ ← 可选的顶部横幅
│ │
├──────────┬────────────────────────┬─────────┤
│ │ │ │
│ Sidebar │ Main Content │ TOC │ ← 文章目录(桌面端右侧)
│ (左侧) │ (中间主内容) │ (右侧) │
│ │ │ │
├──────────┴────────────────────────┴─────────┤
│ Footer │
└─────────────────────────────────────────────┘

关键实现细节:

<!-- 主网格使用 CSS Grid 实现 -->
<div id="main-grid" class="grid grid-cols-[17.5rem_auto] lg:grid-rows-[auto]">
<SideBar class="col-span-2 lg:col-span-1"></SideBar>
<main id="swup-container" class="col-span-2 lg:col-span-1">
<slot></slot> <!-- 页面内容注入点 -->
</main>
</div>

响应式断点策略:

  • lg (1024px+): 三栏布局(侧边栏 + 主内容 + TOC)
  • md (768px+): 两栏布局
  • 移动端: 单栏,侧边栏移至底部

2.2 内容管理系统#

2.2.1 Astro Content Collections#

Fuwari 使用 Astro 的内容集合 API 管理文章:

src/content/config.ts
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(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
// 内部使用的导航字段
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});

2.2.2 文章排序与导航#

getSortedPosts() 函数实现了文章排序和前后篇关联:

src/utils/content-utils.ts
export async function getSortedPosts() {
const sorted = await getRawSortedPosts(); // 按发布日期降序
// 构建前后篇导航
for (let i = 1; i < sorted.length; i++) {
sorted[i].data.nextSlug = sorted[i - 1].slug;
sorted[i].data.nextTitle = sorted[i - 1].data.title;
}
for (let i = 0; i < sorted.length - 1; i++) {
sorted[i].data.prevSlug = sorted[i + 1].slug;
sorted[i].data.prevTitle = sorted[i + 1].data.title;
}
return sorted;
}

2.3 样式系统架构#

2.3.1 CSS 变量体系#

Fuwari 使用 Stylus 定义了一套完整的 CSS 变量系统:

src/styles/variables.styl
:root
--radius-large: 1rem
--primary: oklch(0.70 0.14 var(--hue))
--page-bg: oklch(0.95 0.01 var(--hue))
--card-bg: white
// ... 更多变量
// 暗色模式覆盖
:root.dark
--primary: oklch(0.75 0.14 var(--hue))
--page-bg: oklch(0.16 0.014 var(--hue))
--card-bg: oklch(0.23 0.015 var(--hue))

设计亮点:

  • 使用 OKLCH 色彩空间,支持色相自定义
  • --hue 变量允许用户通过滑块调整主题色
  • 明暗模式通过 CSS 类切换,无需重新加载

2.3.2 Tailwind 集成#

tailwind.config.cjs
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,mjs}"],
darkMode: "class", // 手动切换暗色模式
theme: {
extend: {
fontFamily: {
sans: ["MiSans", "sans-serif", ...defaultTheme.fontFamily.sans],
},
},
},
}

2.4 插件系统#

Fuwari 通过 Remark/Rehype 插件扩展 Markdown 功能:

2.4.1 阅读时间统计#

src/plugins/remark-reading-time.mjs
import { toString } from "mdast-util-to-string";
import getReadingTime from "reading-time";
export function remarkReadingTime() {
return (tree, { data }) => {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
// 注入到 frontmatter
data.astro.frontmatter.minutes = Math.max(1, Math.round(readingTime.minutes));
data.astro.frontmatter.words = readingTime.words;
};
}

2.4.2 文章摘要生成#

src/plugins/remark-excerpt.js
export function remarkExcerpt() {
return (tree, { data }) => {
let excerpt = "";
// 遍历 AST,提取前 150 个字符
for (const node of tree.children) {
if (node.type === "paragraph") {
excerpt += extractText(node);
if (excerpt.length > 150) break;
}
}
data.astro.frontmatter.excerpt = excerpt.slice(0, 150) + "...";
};
}

2.4.3 自定义指令组件#

支持在 Markdown 中使用自定义指令:

:::github{repo="facebook/react"}
:::
:::tip
这是一个提示框
:::

实现原理:

astro.config.mjs
rehypePlugins: [
[rehypeComponents, {
components: {
github: GithubCardComponent,
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
// ...
}
}]
]

三、核心功能实现详解#

3.1 文章卡片组件(PostCard)#

这是博客列表的核心组件,实现了响应式双模式布局

桌面端布局:

┌─────────────────────────────────────────┬─────────┐
│ │ │
│ Title │ Cover │
│ │ Image │
│ Category • Date │ (右侧) │
│ │ │
│ Description excerpt... │ │
│ │ │
│ 1,234 words | 5 min │ │
│ │ │
└─────────────────────────────────────────┴─────────┘

移动端布局:

┌─────────────────────────────────────────┐
│ Cover Image (顶部) │
├─────────────────────────────────────────┤
│ Title > │
│ Category • Date │
│ Description... │
│ 1,234 words | 5 min │
└─────────────────────────────────────────┘

关键代码实现:

<!-- 响应式布局切换 -->
<div class="flex flex-col-reverse md:flex-col ...">
<!-- 内容区域:桌面端左侧,移动端下方 -->
<div class="w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]">
<a href={url} class="group ...">
{title}
<!-- 悬停动画效果 -->
<Icon class="opacity-0 group-hover:opacity-100 ..." />
</a>
<PostMetadata ... />
<div class="line-clamp-2 md:line-clamp-1">{description}</div>
</div>
<!-- 封面区域:桌面端右侧绝对定位,移动端顶部 -->
{hasCover && <a class="md:absolute md:right-3 ...">
<ImageWrapper src={image} ... />
</a>}
</div>

3.2 侧边栏组件系统#

侧边栏由多个可复用的小部件组成:

3.2.1 个人资料卡片(Profile)#

<div class="card-base p-3">
<!-- 头像区域 -->
<a href="/about/" class="group block relative ...">
<div class="absolute ... group-hover:bg-black/30">
<Icon name="fa6-regular:address-card"
class="opacity-0 group-hover:opacity-100" />
</div>
<ImageWrapper src={config.avatar} ... />
</a>
<!-- 信息区域 -->
<div class="text-center">
<div class="font-bold text-xl">{config.name}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full">
</div> <!-- 装饰性分隔线 -->
<div class="text-neutral-400">{config.bio}</div>
<!-- 社交链接 -->
<div class="flex flex-wrap gap-2 justify-center">
{config.links.map(item =>
<a href={item.url} class="btn-regular rounded-lg h-10 w-10">
<Icon name={item.icon} />
</a>
)}
</div>
</div>
</div>

3.2.2 分类和标签组件#

Categories.astro
<div class="flex flex-wrap gap-2">
{categories.map(category =>
<a href={category.url}
class="btn-regular rounded-full px-3 py-1 text-sm">
{category.name} {category.count}
</a>
)}
</div>

3.3 文章详情页实现#

文章页是功能最复杂的页面,包含:

3.3.1 动态路由生成#

src/pages/posts/[...slug].astro
---
export async function getStaticPaths() {
const blogEntries = await getSortedPosts();
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content, headings } = await entry.render();
---

3.3.2 文章元数据展示#

文章头部包含:

  • 字数统计和阅读时间(通过 remark 插件计算)
  • 发布/更新日期
  • 分类和标签
  • 封面图片
<div class="flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3">
<div class="flex items-center">
<Icon name="material-symbols:notes-rounded" />
<div>{remarkPluginFrontmatter.words} words</div>
</div>
<div class="flex items-center">
<Icon name="material-symbols:schedule-outline-rounded" />
<div>{remarkPluginFrontmatter.minutes} min read</div>
</div>
</div>

3.3.3 Markdown 渲染#

<Markdown class="mb-6 markdown-content onload-animation">
<Content />
</Markdown>

Markdown.astro 组件封装了:

  • Tailwind Typography 样式 (prose)
  • 代码块复制按钮
  • 图片点击放大(PhotoSwipe)
  • 锚点链接生成

3.4 搜索功能实现#

Fuwari 使用 Pagefind 实现静态搜索:

3.4.1 构建时索引生成#

package.json
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}

3.4.2 前端搜索组件#

Search.svelte
<script>
let keyword = "";
let results = [];
async function search() {
if (!window.pagefind) return;
const response = await window.pagefind.search(keyword);
results = await Promise.all(
response.results.map(item => item.data())
);
}
</script>
<div id="search-bar">
<input bind:value={keyword} on:input={search} placeholder="Search..." />
</div>
<div id="search-results">
{#each results as item}
<a href={item.url}>
<div>{item.meta.title}</div>
<div>{@html item.excerpt}</div>
</a>
{/each}
</div>

四、扩展开发指南#

4.1 添加新页面#

以添加”友链”页面为例:

步骤 1: 创建页面组件#

src/pages/friends.astro
---
import Layout from "@/layouts/Layout.astro";
---
<Layout title="友链 | Friends">
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-8 text-center">✨ 友情链接</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 友链卡片 -->
<a href="https://example.com"
class="card-base p-4 flex items-center gap-4
hover:border-[var(--primary)] transition-all">
<img src="avatar.jpg" class="w-12 h-12 rounded-full" />
<div>
<h3 class="font-semibold">站点名称</h3>
<p class="text-sm text-[var(--text-muted)]">站点描述</p>
</div>
</a>
</div>
</div>
</Layout>

步骤 2: 添加到导航栏#

src/config.ts
export const navBarConfig: NavBarConfig = {
links: [
LinkPreset.Home,
LinkPreset.Archive,
{
name: "友链✨",
url: "/friends/",
},
LinkPreset.About,
],
};

4.2 自定义主题色#

Fuwari 支持动态调整主题色相:

src/config.ts
export const siteConfig: SiteConfig = {
themeColor: {
hue: 250, // 默认色相 (0-360)
fixed: false, // 是否隐藏主题色选择器
},
};

实现原理:

  • 所有颜色使用 oklch() 函数定义
  • 色相通过 CSS 变量 --hue 控制
  • 用户调整时更新 localStorage 和 CSS 变量

4.3 添加自定义组件#

步骤 1: 创建组件#

src/components/MyComponent.astro
---
interface Props {
title: string;
content: string;
}
const { title, content } = Astro.props;
---
<div class="card-base p-6 my-4 border-l-4 border-[var(--primary)]">
<h3 class="text-xl font-bold mb-2">{title}</h3>
<p class="text-[var(--text-muted)]">{content}</p>
</div>

步骤 2: 在 Markdown 中使用#

通过 rehype-components 插件注册:

astro.config.mjs
import { MyComponent } from "./src/components/MyComponent.astro";
rehypePlugins: [
[rehypeComponents, {
components: {
mycomponent: MyComponent,
}
}]
]

Markdown 中使用:

:::mycomponent{title="提示" content="这是自定义组件"}
:::

4.4 修改文章 Schema#

添加自定义字段:

src/content/config.ts
const postsCollection = defineCollection({
schema: z.object({
// ... 原有字段
author: z.string().optional().default("默认作者"),
difficulty: z.enum(["简单", "中等", "困难"]).optional(),
series: z.string().optional(), // 系列文章
}),
});

在模板中使用:

<!-- 显示难度标签 -->
{entry.data.difficulty && (
<span class="badge">
{entry.data.difficulty}
</span>
)}

五、性能优化策略#

5.1 Islands 架构#

Astro 的 Islands 架构确保:

  • 静态内容在构建时生成 HTML
  • 交互组件(如搜索)按需 hydrate
  • 减少客户端 JavaScript 体积
<!-- 静态渲染 -->
<PostCard entry={entry} />
<!-- 客户端交互组件 -->
<Search client:only="svelte" />

5.2 图片优化#

<!-- ImageWrapper 组件封装了 Sharp 优化 -->
<ImageWrapper
src="large-image.jpg"
alt="描述"
class="w-full h-auto"
/>

5.3 字体加载策略#

/* 使用 font-display: swap 防止 FOIT */
@font-face {
font-family: 'MiSans';
src: url('/fonts/MiSans-Normal.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* 关键:先显示后备字体 */
}

六、部署与构建#

6.1 构建流程#

Terminal window
# 1. 类型检查
pnpm run type-check
# 2. 构建 Astro 站点
astro build
# 3. 生成搜索索引
pagefind --site dist
# 4. 输出目录: dist/

6.2 部署配置#

Vercel 配置:

vercel.json
{
"framework": "astro"
}

GitHub Actions:

name: Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm build
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist

七、总结#

Fuwari 的架构设计体现了以下优秀实践:

  1. 组件化设计:可复用的卡片、按钮、布局组件
  2. 配置驱动:通过 config.ts 集中管理站点配置
  3. 类型安全:全面的 TypeScript 类型定义
  4. 性能优先:静态生成、图片优化、按需加载
  5. 可扩展性:插件系统支持自定义 Markdown 组件
  6. 国际化:完整的 i18n 支持

通过理解这些架构设计,你可以:

  • 自信地修改和扩展主题功能
  • 添加自定义页面和组件
  • 优化性能和用户体验
  • 迁移到其他 Astro 项目

参考资源:

Fuwari 博客主题深度架构解析:从源码到自定义开发
https://fuwari.vercel.app/posts/tech/fuwari-architecture-deep-dive/
作者
焕然一新
发布于
2026-03-12
许可协议
CC BY-NC-SA 4.0