基于Halo Fuwari 主题的自定义功能重构:Astro 化改造 + 视频放映室 + 音乐播放器
基于Halo Fuwari 主题的自定义功能重构:Astro 化改造 + 视频放映室 + 音乐播放器
前言
之前对 Halo Fuwari 主题的视频放映室功能做了一版「手术刀式」改造——直接编辑 templates/*.html,在 page.html 里塞了 270 行 JS 实现视频播放。能用,但维护困难,且无法跟进上游更新。
这次做了一次彻底的架构升级:跟随原项目的重构,将主题迁移到上游的 Astro 6 构建流程,同时保留了我们所有的自定义功能。以下是完整的技术记录。
一、架构变更总览
之前
templates/page.html ← 直接编辑,270行JS内联
templates/index.html ← 侧边栏归档硬编码
settings.yaml ← 手动添加 video 配置
问题:
- 无法跟进上游更新(音乐播放器、Admonition 等新功能)
- 代码耦合度高,改一处牵动全局
- 没有组件化,样式和逻辑混在一起
之后
src/ ← Astro 源码
├── components/
│ ├── video/
│ │ └── VideoCinema.astro ← 视频放映室独立组件
│ └── widget/
│ └── SideBar.astro ← 新增 archives widget
├── pages/
│ ├── page.astro ← slug 条件路由
│ └── photos.astro ← masonry 布局
├── styles/
│ └── video-cinema.css ← 视频样式独立文件
└── layouts/
└── Layout.astro ← CSS/i18n 全局注入
pnpm build → templates/ ← 构建产物,上传到 Halo
优势:
- 组件化开发,视频、音乐、归档各管各的
pnpm build一键构建,输出纯 HTML 给 Halo- 可以随时
git merge upstream/main同步上游更新
二、视频放映室组件化(VideoCinema.astro)
核心改动
将原来 page.html 中的内联代码迁移到 Astro 组件 src/components/video/VideoCinema.astro。
Astro 对 Halo 主题的支持方式很巧妙:Astro 文件中的 Thymeleaf 指令(th:if、th:text 等)在编译后原样保留,Halo 依然能用服务端渲染。
<!-- page.astro 中的路由判断 -->
<div th:if="${singlePage.spec.slug == 'videos'}">
<VideoCinema />
</div>
<div th:if="${singlePage.spec.slug != 'videos'}">
<!-- 普通页面内容 -->
</div>
JS 逻辑的处理
Astro 中需要用 <script is:inline> 来保留原始 JS(不做模块化处理),配合 define:vars 注入配置:
<script is:inline define:vars={{ videoConfig: "${theme.config.video}" }}>
(() => {
const themeConfig = JSON.parse(
document.getElementById("theme-config").textContent
);
const videoList = themeConfig.video.videos || [];
// ... 视频播放逻辑
})();
</script>
这种模式参照了上游的 MusicPlayer.astro,保持了代码风格一致。
CDN 直连播放优化
视频播放架构做了优化,服务器只做"指路",不搬运视频流:
优化前:浏览器 → 服务器(代理) → CDN (服务器带宽瓶颈)
优化后:浏览器 → CDN 直连 (服务器零带宽消耗)
实现方式:
<meta name="referrer" content="no-referrer">— 破解 Referer 防盗链- DPlayer 直接播放 API 返回的 CDN 直链,不经服务器代理
对于 CORS 限制的 CDN(如腾讯云),仍走 Nginx 代理。
三、server.js 解析服务增强
HLS (m3u8) 视频支持
部分影视网站使用 HLS 流而非 MP4 直链,之前的 server.js 无法捕获 m3u8 地址。三处修复:
1. 从中间 URL 参数提取真实视频地址
中间URL: https://yun.92cj.com/mp4hls/?vid=https://xxx/index.m3u8
↑ 直接提取这个参数
// 从中间播放器 URL 的参数中提取真实视频地址
const vidMatch = url.match(/[?&]vid=(https?:\/\/[^&]+)/i);
if (vidMatch) {
const realUrl = decodeURIComponent(vidMatch[1]);
if (isTargetVideo(realUrl, type)) {
resolved = true;
resolve(realUrl);
return;
}
}
2. 扩展 Content-Type 匹配
// 之前:只匹配 video/mp4 和 octet-stream
// 现在:匹配所有 video/* + mpegurl + octet-stream
if (ct.includes('video/') || ct.includes('mpegurl') || ct.includes('octet-stream'))
3. 补充点击选择器
// 新增 div[class*="play"] 和 .video-play
const selectors = [
'iframe', '.play-btn', 'button[class*="play"]',
'div[class*="play"]', '.video-play', // ← 新增
...
];
HLS.js 播放支持
前端新增 hls.js 加载,DPlayer 可以播放 m3u8 流:
<script src="https://cdn.jsdelivr.net/npm/hls.js/dist/hls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>
四、上游新功能合并
通过 Astro 构建流程,自动获得了上游的所有更新:
侧边栏音乐播放器
支持网易云、QQ 音乐、酷狗等平台,有歌词显示、播放列表、播放模式切换。
音乐 API 需要通过 Nginx 反代解决 CORS:
location /meting-api/ {
proxy_pass https://api.injahow.cn/meting/;
proxy_set_header Host api.injahow.cn;
proxy_ssl_server_name on;
add_header Access-Control-Allow-Origin * always;
}
主题设置中自定义 Meting API 填入 /meting-api/?server=:server&type=:type&id=:id&r=:r。
其他上游更新
- Admonition 提示块 — 支持
[!NOTE]、[!TIP]、[!WARNING]等语法 - Iconify 自定义图标 — 社交媒体图标支持 Iconify 图标库
- 配置转义修复 — HTML 配置不再被转义
五、侧边栏归档 Widget
将之前的硬编码归档改为标准 widget 类型,在 SideBar.astro 中注册:
<div class="contents" th:case="'archives'">
<div th:with="recentPosts = ${postFinder.list(1, 10)}">
<!-- 最近 10 篇文章列表 -->
<a th:each="post : ${recentPosts.items}"
th:href="@{${post.status.permalink}}">
<span th:text="${post.spec.title}">Post</span>
<span th:text="${#dates.format(post.spec.publishTime, 'MM-dd')}">01-01</span>
</a>
<a th:href="@{/archives}">查看全部</a>
</div>
</div>
在 settings.yaml 中注册 widget 选项,用户可以在后台自由拖拽排序。
六、图库瀑布流布局
一行 CSS 改动,从固定网格改为瀑布流:
- class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"
+ class="columns-1 sm:columns-2 xl:columns-3 gap-4"
同时需要在每个 <figure> 上添加 break-inside-avoid 防止卡片跨列断裂。
七、开发与部署流程
本地开发
pnpm install # 安装依赖(需要 Node.js >= 22)
pnpm dev # 开发模式,文件变更自动重新构建
pnpm build:only # 构建,输出到 templates/
部署
# 构建
pnpm build:only
# 上传到服务器 Halo 主题目录
scp -P 端口 -r templates/ settings.yaml theme.yaml i18n/ \
root@服务器:/path/to/halo/themes/theme-fuwari/
# 重启 Halo
docker restart halo
同步上游
git fetch upstream
git merge upstream/main
# 解决冲突后重新构建
pnpm build:only
八、技术栈版本
| 依赖 | 版本 |
|---|---|
| Halo | >= 2.24.0 |
| Astro | 6.x |
| Node.js(开发) | >= 22.12.0 |
| Node.js(服务器) | 18.x(仅 server.js 解析服务) |
| Tailwind CSS | 4.x |
| DPlayer | 最新(CDN) |
| HLS.js | 最新(CDN) |
| Playwright | >= 1.40 |
九、文件改动清单
| 文件 | 操作 | 说明 |
|---|---|---|
src/components/video/VideoCinema.astro | 新建 | 视频放映室组件 |
src/styles/video-cinema.css | 新建 | 视频卡片/播放器样式 |
src/pages/page.astro | 修改 | slug 路由 + meta referrer |
src/layouts/Layout.astro | 修改 | CSS 引入 + i18n key |
src/components/widget/SideBar.astro | 修改 | 新增 archives widget |
src/pages/photos.astro | 修改 | masonry 布局 |
settings.yaml | 修改 | video 组 + archives 选项 |
i18n/*.properties (3个) | 修改 | video + archives 翻译 |
m3u8-crawler/server.js | 修改 | m3u8 支持 + vid 提取 |
十、Nginx 完整配置参考
server {
listen 443 ssl;
server_name blog.fallrain0905.top;
# Halo 主站
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 视频解析 API → Node.js
location /api/getVideo {
proxy_pass http://127.0.0.1:3000/api/getVideo;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# B站 API 代理
location /bili-api/ {
proxy_pass https://api.bilibili.com/;
proxy_set_header Host api.bilibili.com;
proxy_set_header Referer "https://www.bilibili.com";
proxy_set_header User-Agent "Mozilla/5.0";
proxy_ssl_server_name on;
}
# 音乐 Meting API 代理
location /meting-api/ {
proxy_pass https://api.injahow.cn/meting/;
proxy_set_header Host api.injahow.cn;
proxy_ssl_server_name on;
add_header Access-Control-Allow-Origin * always;
}
}