第七章:fuwari - Mizuki 主题魔改

第七章:fuwari - Mizuki 主题魔改

本章记录对 Mizuki 主题的定制化修改,包括功能增强和问题修复。

7.1 修复 Umami 自搭建实例支持

7.1.1 问题说明

Mizuki 主题自带的 umami-share.js 文件存在以下问题:

  1. 仅支持 Umami Cloud:原代码硬编码了 /v1/websites/ API 路径和 x-umami-api-key 认证方式,只适用于 Umami Cloud 官方服务
  2. 不支持自搭建实例:自搭建的 Umami 实例使用不同的 API 路径(/api/websites/)和认证方式(Authorization: Bearer),导致配置后出现 404 或 401 错误
  3. 无法自动识别实例类型:代码无法自动判断是使用 Cloud 还是自搭建实例,需要手动修改代码

7.1.2 解决方案

第一步:获取 Bearer Token

  1. 登录你的 Umami 管理后台
  2. 打开浏览器开发者工具(F12)→ Network 标签页
  3. 刷新页面,找到对 /api/auth/verify 的请求
  4. 在请求头中找到 Authorization: Bearer <token>
  5. 复制 <token> 部分(不含 "Bearer " 前缀)

第二步:配置 API Key

src/config.ts 中更新 umamiConfig

1
2
3
4
5
6
7
8
export const umamiConfig = {
enabled: true,
apiKey: import.meta.env.UMAMI_API_KEY || "你的Bearer Token", // 这里填写从步骤 5 复制的 token
baseUrl: "https://umami.prorise666.site/api", // 你的 Umami 实例地址
scripts: `
<script defer src="https://umami.prorise666.site/script.js" data-website-id="你的网站ID"></script>
`.trim(),
} as const;

第三步:替换 umami-share.js 文件

将以下代码完整复制,替换 public/js/umami-share.js 文件的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
((global) => {
const cacheKey = "umami-share-cache";
const cacheTTL = 3600_000; // 1h

/**
* 判断是否为自搭建实例
* @param {string} baseUrl - Umami API基础URL
* @returns {boolean} 如果是自搭建实例返回 true,否则返回 false
*/
function isSelfHostedInstance(baseUrl) {
// 判断规则:
// 1. 如果 baseUrl 包含 api.umami.is,则是 Cloud(官方服务)
// 2. 如果 baseUrl 以 /api 结尾,通常是自搭建
// 3. 如果 baseUrl 包含 /api 但不包含 /v1,通常是自搭建
const isCloud = baseUrl.includes('api.umami.is');
return !isCloud && (baseUrl.endsWith('/api') || (baseUrl.includes('/api') && !baseUrl.includes('/v1')));
}

/**
* 获取网站统计数据
* @param {string} baseUrl - Umami API基础URL(支持 Umami Cloud 和自搭建实例)
* @param {string} apiKey - API密钥或Bearer Token
* @param {string} websiteId - 网站ID
* @returns {Promise<object>} 网站统计数据
*/
async function fetchWebsiteStats(baseUrl, apiKey, websiteId) {
// 检查缓存
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const parsed = JSON.parse(cached);
if (Date.now() - parsed.timestamp < cacheTTL) {
return parsed.value;
}
} catch {
localStorage.removeItem(cacheKey);
}
}

const currentTimestamp = Date.now();
const isSelfHosted = isSelfHostedInstance(baseUrl);

// 自搭建实例使用 /api/websites/,Cloud 使用 /api/v1/websites/
const apiPath = isSelfHosted
? `/websites/${websiteId}/stats`
: `/v1/websites/${websiteId}/stats`;
const statsUrl = `${baseUrl}${apiPath}?startAt=0&endAt=${currentTimestamp}`;

// 自搭建实例使用 Authorization: Bearer,Cloud 使用 x-umami-api-key
// 如果 apiKey 已经包含 "Bearer " 前缀,则不再添加
const authValue = isSelfHosted
? (apiKey.startsWith('Bearer ') ? apiKey : `Bearer ${apiKey}`)
: apiKey;
const headers = isSelfHosted
? { "Authorization": authValue }
: { "x-umami-api-key": apiKey };

const res = await fetch(statsUrl, { headers });

if (!res.ok) {
const errorText = await res.text().catch(() => '');
throw new Error(`获取网站统计数据失败: HTTP ${res.status} ${res.statusText}${errorText ? ' - ' + errorText : ''}`);
}

const stats = await res.json();

// 缓存结果
localStorage.setItem(
cacheKey,
JSON.stringify({ timestamp: Date.now(), value: stats }),
);

return stats;
}

/**
* 获取特定页面的统计数据
* @param {string} baseUrl - Umami API基础URL(支持 Umami Cloud 和自搭建实例)
* @param {string} apiKey - API密钥或Bearer Token
* @param {string} websiteId - 网站ID
* @param {string} urlPath - 页面路径
* @param {number} startAt - 开始时间戳
* @param {number} endAt - 结束时间戳
* @returns {Promise<object>} 页面统计数据
*/
async function fetchPageStats(
baseUrl,
apiKey,
websiteId,
urlPath,
startAt = 0,
endAt = Date.now(),
) {
const isSelfHosted = isSelfHostedInstance(baseUrl);

// 自搭建实例使用 /api/websites/,Cloud 使用 /api/v1/websites/
const apiPath = isSelfHosted
? `/websites/${websiteId}/stats`
: `/v1/websites/${websiteId}/stats`;
const statsUrl = `${baseUrl}${apiPath}?startAt=${startAt}&endAt=${endAt}&path=${encodeURIComponent(urlPath)}`;

// 自搭建实例使用 Authorization: Bearer,Cloud 使用 x-umami-api-key
// 如果 apiKey 已经包含 "Bearer " 前缀,则不再添加
const authValue = isSelfHosted
? (apiKey.startsWith('Bearer ') ? apiKey : `Bearer ${apiKey}`)
: apiKey;
const headers = isSelfHosted
? { "Authorization": authValue }
: { "x-umami-api-key": apiKey };

const res = await fetch(statsUrl, { headers });

if (!res.ok) {
const errorText = await res.text().catch(() => '');
throw new Error(`获取页面统计数据失败: HTTP ${res.status} ${res.statusText}${errorText ? ' - ' + errorText : ''}`);
}

return await res.json();
}

/**
* 获取 Umami 网站统计数据
* @param {string} baseUrl - Umami API基础URL(支持 Umami Cloud 和自搭建实例)
* @param {string} apiKey - API密钥或Bearer Token
* @param {string} websiteId - 网站ID
* @returns {Promise<object>} 网站统计数据
*/
global.getUmamiWebsiteStats = async (baseUrl, apiKey, websiteId) => {
try {
return await fetchWebsiteStats(baseUrl, apiKey, websiteId);
} catch (err) {
throw new Error(`获取Umami统计数据失败: ${err.message}`);
}
};

/**
* 获取特定页面的 Umami 统计数据
* @param {string} baseUrl - Umami API基础URL(支持 Umami Cloud 和自搭建实例)
* @param {string} apiKey - API密钥或Bearer Token
* @param {string} websiteId - 网站ID
* @param {string} urlPath - 页面路径
* @param {number} startAt - 开始时间戳(可选)
* @param {number} endAt - 结束时间戳(可选)
* @returns {Promise<object>} 页面统计数据
*/
global.getUmamiPageStats = async (
baseUrl,
apiKey,
websiteId,
urlPath,
startAt,
endAt,
) => {
try {
return await fetchPageStats(
baseUrl,
apiKey,
websiteId,
urlPath,
startAt,
endAt,
);
} catch (err) {
throw new Error(`获取Umami页面统计数据失败: ${err.message}`);
}
};

global.clearUmamiShareCache = () => {
localStorage.removeItem(cacheKey);
};
})(window);

修复后的功能:

  • ✅ 自动识别 Umami Cloud 和自搭建实例
  • ✅ 自动使用正确的 API 路径(/api/websites//api/v1/websites/
  • ✅ 自动使用正确的认证方式(Authorization: Bearerx-umami-api-key
  • ✅ 支持 API Key 已包含 "Bearer " 前缀的情况
  • ✅ 改进的错误提示,便于调试

7.2 配置默认主题(亮色/暗色)

7.2.1 功能说明

Mizuki 主题默认使用亮色主题,但可以通过配置项设置默认主题为暗色或亮色。当用户首次访问时,会使用配置的默认主题;如果用户之前手动切换过主题,则优先使用用户的选择。

7.2.2 修改步骤

第一步:修改类型定义文件

文件路径:src/types/config.ts

定位到 themeColor 配置项(约第 55-58 行),在其后添加:

1
2
3
4
5
6
7
themeColor: {
hue: number;
fixed: boolean;
};

// 默认主题配置
defaultTheme?: "light" | "dark"; // 默认主题:light=亮色主题,dark=暗色主题。如果不设置,则使用系统偏好或默认亮色主题

第二步:修改配置文件

文件路径:src/config.ts

定位到 themeColor 配置项(约第 32-35 行),在其后添加:

1
2
3
4
5
6
themeColor: {
hue: 360, // 主题色的默认色相,范围从 0 到 360。例如:红色:0,青色:200,蓝绿色:250,粉色:345
fixed: false, // 对访问者隐藏主题色选择器
},

defaultTheme: "dark", // 默认主题:"light" 亮色主题,"dark" 暗色主题。如果不设置,则使用系统偏好或默认亮色主题

第三步:修改布局文件

文件路径:src/layouts/Layout.astro

  1. 定位到约第 240 行,找到:
1
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue}}>

修改为:

1
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue, defaultTheme: siteConfig.defaultTheme || DEFAULT_THEME}}>
  1. 定位到约第 242 行,找到:
1
const theme = localStorage.getItem('theme') || DEFAULT_THEME;

修改为:

1
const theme = localStorage.getItem('theme') || defaultTheme || DEFAULT_THEME;
  1. 定位到约第 881 行,找到:
1
const storedTheme = localStorage.getItem('theme') || DEFAULT_THEME;

修改为:

1
const storedTheme = localStorage.getItem('theme') || (siteConfig.defaultTheme || DEFAULT_THEME);

第四步:修改工具函数

文件路径:src/utils/setting-utils.ts

定位到约第 143-145 行,找到:

1
2
3
export function getStoredTheme(): LIGHT_DARK_MODE {
return (localStorage.getItem("theme") as LIGHT_DARK_MODE) || DEFAULT_THEME;
}

修改为:

1
2
3
4
5
6
export function getStoredTheme(): LIGHT_DARK_MODE {
const stored = localStorage.getItem("theme") as LIGHT_DARK_MODE;
if (stored) return stored;
// 如果配置中有默认主题,使用配置的;否则使用常量中的默认值
return (siteConfig.defaultTheme as LIGHT_DARK_MODE) || DEFAULT_THEME;
}

7.2.3 配置说明

src/config.ts 中,你可以这样配置:

1
2
3
4
5
6
7
export const siteConfig: SiteConfig = {
// ... 其他配置
defaultTheme: "dark", // 默认暗色主题
// 或
defaultTheme: "light", // 默认亮色主题
// 如果不设置,则使用系统默认(亮色主题)
}

优先级说明:

  1. 用户手动切换的主题(存储在 localStorage)
  2. 配置中的 defaultTheme
  3. 系统默认值(亮色主题)

7.3 将 About 页面改造为 GitHub README 风格

7.3.1 功能说明

Mizuki 主题的 About 页面默认是简单的 Markdown 内容展示。本魔改将 About 页面改造为类似 GitHub README 的风格,支持:

  • 动态打字效果
  • 社交平台徽章展示
  • 技能图标展示
  • GitHub 活动统计图表
  • 全宽图片和居中对齐
  • 响应式布局

7.3.2 修改步骤

第一步:替换 about.md 内容

文件路径:src/content/spec/about.md

将以下模板复制到 about.md,并根据提示填写你的个人信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!-- 网站介绍(可选) -->
This website is built with the **Astro** framework using the [Mizuki](https://github.com/matsuzaka-yuki/mizuki) theme.

::github{repo="matsuzaka-yuki/Mizuki"}

<!-- 顶部装饰线 -->
<div align="center">
<img src="https://capsule-render.vercel.app/api?type=soft&color=gradient&customColorList=12&height=3" width="100%" />
</div>

<!-- 个人介绍横幅 -->
<div align="center">
<!-- 修改:将 YOUR_USERNAME 替换为你的 GitHub 用户名 -->
<img src="https://capsule-render.vercel.app/api?type=waving&color=0:000000,25:282424,50:504949,75:786d6d,100:9e9494&height=250&section=header&text=YOUR_USERNAME&fontSize=90&fontAlignY=35&animation=fadeIn&fontColor=white&desc=你的个性签名&descAlignY=55" />

<!-- 修改:将 YOUR_NAME 替换为你的名字 -->
# 👋 Hello,here is YOUR_NAME

<!-- 修改:自定义打字效果内容,lines= 后面用分号分隔多行 -->
[![Typing SVG](https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=24&duration=2000&pause=500&color=2C9CDF&center=true&vCenter=true&width=600&lines=第一行内容;第二行内容;第三行内容)](https://git.io/typing-svg)

<!-- 社交平台徽章 -->
<p align="center">
<!-- 修改:添加或删除你需要的社交平台链接 -->
<a href="https://github.com/YOUR_USERNAME" style="text-decoration: none;">
<img src="https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white" alt="GitHub" style="margin: 2px;"/>
</a>
<a href="你的链接" style="text-decoration: none;">
<img src="https://img.shields.io/badge/平台名称-颜色代码?style=for-the-badge&logo=logo名称&logoColor=white" alt="平台名称" style="margin: 2px;"/>
</a>
<!-- 更多徽章... -->
</p>

<!-- 统计信息 -->
<p>
<!-- 修改:将 YOUR_USERNAME 替换为你的 GitHub 用户名 -->
<img src="https://komarev.com/ghpvc/?username=YOUR_USERNAME&style=for-the-badge&color=blueviolet" alt="访问计数器" />
<img src="https://img.shields.io/github/followers/YOUR_USERNAME?style=for-the-badge&color=FF5F6D&labelColor=141321" alt="关注者" />
<img src="https://img.shields.io/github/stars/YOUR_USERNAME?style=for-the-badge&color=FFC371&labelColor=141321" alt="星标" />
</p>
<p>
<img src="https://count.getloli.com/get/@YOUR_USERNAME?theme=rule34" alt="访问计数器" />
</p>
</div>

<!-- 分隔线 -->
<img width="100%" src="https://capsule-render.vercel.app/api?type=rect&color=gradient&height=2" />

<!-- 技能与工具 -->
<h2 align="center">
<img src="https://media2.giphy.com/media/QssGEmpkyEOhBCb7e1/giphy.gif?cid=ecf05e47a0n3gi1bfqntqmob8g9aid1oyj2wr3ds3mg700bl&rid=giphy.gif" width="30px" height="30px" style="vertical-align: middle; position: relative; top: -2px;"/>
技能与工具
</h2>
<div align="center">
<!-- 修改:skillicons.dev 支持的技术栈图标,i= 后面用逗号分隔技术名称 -->
<!-- 访问 https://skillicons.dev/ 查看所有支持的图标 -->
<img src="https://skillicons.dev/icons?i=html,css,javascript,typescript,vue,react,nodejs&perline=15" />
<img src="https://skillicons.dev/icons?i=python,java,go,rust&perline=15" />
<img src="https://skillicons.dev/icons?i=mysql,mongodb,redis,postgres&perline=15" />
<img src="https://skillicons.dev/icons?i=linux,docker,nginx,git,github&perline=15" />
<br/>
<!-- 修改:自定义你的工具描述 -->
<p>🚀 <b>你的工具描述:</b> 工具1 | 工具2 | 工具3</p>
</div>

<!-- 分隔线 -->
<div align="center">
<img src="https://capsule-render.vercel.app/api?type=soft&color=gradient&customColorList=12&height=3" width="100%" />
</div>

<!-- GitHub 活动 -->
<h2 align="center">
<img src="https://i.imgur.com/dBaSKWF.gif" height="25px" width="25px" style="vertical-align: middle; position: relative; top: -2px;"/>
GitHub 活动
<img src="https://i.imgur.com/dBaSKWF.gif" height="25px" width="25px" style="vertical-align: middle; position: relative; top: -2px;"/>
</h2>

<div align="center">
<!-- 修改:将 YOUR_USERNAME 替换为你的 GitHub 用户名 -->
<!-- GitHub 贡献图(支持暗色/亮色主题自动切换) -->
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/YOUR_USERNAME/YOUR_USERNAME/output/github-contribution-grid-snake-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/YOUR_USERNAME/YOUR_USERNAME/output/github-contribution-grid-snake.svg">
<img alt="github contribution grid snake animation" src="https://raw.githubusercontent.com/YOUR_USERNAME/YOUR_USERNAME/output/github-contribution-grid-snake.svg" width="100%">
</picture>
<!-- GitHub 统计卡片 -->
<img src="https://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=YOUR_USERNAME&theme=radical" width="100%" />
</div>

<!-- 自定义内容区域 -->
<div align="center">
<!-- 修改:可以添加自定义的打字效果、名言等 -->
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=22&pause=1000&color=FF9190&center=true&vCenter=true&random=false&width=600&height=80&lines=第一行;第二行;第三行" alt="自定义内容" />
</div>

<!-- 结尾 -->
<h3 align="center">
<img src="https://emojis.slackmojis.com/emojis/images/1531849430/4246/blob-sunglasses.gif?1531849430" width="30px" height="30px" style="vertical-align: middle; position: relative; top: -2px;"/>
感谢访问我的个人主页!
<img src="https://emojis.slackmojis.com/emojis/images/1531849430/4246/blob-sunglasses.gif?1531849430" width="30px" height="30px" style="vertical-align: middle; position: relative; top: -2px;"/>
</h3>

<p align="center">
<i>你的个性签名✨</i>
</p>

模板使用说明:

  1. YOUR_USERNAME:替换为你的 GitHub 用户名
  2. YOUR_NAME:替换为你的名字或昵称
  3. 社交平台徽章:访问 shields.io 生成自定义徽章
  4. 技能图标:访问 skillicons.dev 查看所有支持的图标
  5. GitHub 活动图:需要配置 GitHub Actions 自动生成,参考 github-readme-streak-stats

第二步:修改 about.astro 文件

文件路径:src/pages/about.astro

修改原因: 需要为 Markdown 组件添加 about-page 类名,以便应用专门的样式。

定位到约第 36 行,找到:

1
2
3
<Markdown class="mt-2">
<Content />
</Markdown>

修改为:

1
2
3
<Markdown class="mt-2 about-page">
<Content />
</Markdown>

修改说明:

  • 添加 about-page 类名,用于在 CSS 中针对 About 页面应用特殊样式
  • 这样可以确保样式只影响 About 页面,不会影响其他页面的 Markdown 内容

第三步:添加 CSS 样式

文件路径:src/styles/markdown.css

修改原因: GitHub README 风格的页面需要特殊的样式支持,包括:

  • 居中对齐的元素(div, p, h2, h3)
  • 全宽图片的处理
  • 链接内图片的悬停效果
  • 特殊图片的间距控制

在文件末尾(约第 150 行后)添加以下样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* About 页面特殊样式 - 兼容 GitHub README 风格 */
.custom-md.about-page {
/* 居中对齐的元素 */
div[align="center"],
p[align="center"],
h2[align="center"],
h3[align="center"] {
@apply text-center;
}

/* 居中对齐的 div 容器 */
div[align="center"] {
@apply flex flex-col items-center justify-center;

p {
@apply text-center;
}

a {
@apply inline-block;
}
}

/* 图片样式 */
img {
@apply max-w-full h-auto;
display: inline-block;

/* 全宽图片 */
&[width="100%"] {
width: 100% !important;
max-width: 100% !important;
}
}

/* 链接内的图片(徽章) */
a img {
@apply transition-transform hover:scale-105;
margin: 2px;
}

/* capsule-render 图片间距 */
img[src*="capsule-render"] {
@apply my-4;
}

/* skillicons 图片间距 */
img[src*="skillicons"] {
@apply my-2;
}
}

样式说明:

  1. 居中对齐样式:支持 align="center" 属性的 div、p、h2、h3 元素自动居中对齐
  2. 图片样式
    • 所有图片默认响应式(max-w-full h-auto
    • width="100%" 的图片会强制全宽显示
  3. 徽章悬停效果:链接内的图片(徽章)在悬停时会轻微放大
  4. 特殊图片间距
    • capsule-render 图片上下间距为 my-4
    • skillicons 图片上下间距为 my-2

7.3.3 效果预览

完成以上修改后,你的 About 页面将具备:

  • ✅ GitHub README 风格的布局
  • ✅ 动态打字效果
  • ✅ 社交平台徽章展示
  • ✅ 技能图标展示
  • ✅ GitHub 活动统计图表
  • ✅ 响应式设计,适配各种设备

7.3.4 常见问题

Q: 为什么要在 Markdown 组件上添加 about-page 类?

A: 这样可以确保样式只影响 About 页面,不会影响其他页面的 Markdown 内容。使用 .custom-md.about-page 选择器可以精确控制样式作用范围。

Q: 如何自定义徽章样式?

A: 访问 shields.io 可以生成自定义徽章,支持自定义颜色、图标、文字等。

Q: GitHub 活动图不显示怎么办?

A: 需要配置 GitHub Actions 自动生成活动图。可以参考 github-readme-streak-stats 的文档进行配置。

Q: 如何添加更多技能图标?

A: 访问 skillicons.dev 查看所有支持的图标,然后在 i= 参数中添加技术名称,用逗号分隔。