Hugo 博客 + GitHub Pages 完整部署方案
★环境准备
- 设备:MacBook(macOS)
- 工具:
- Git
- Hugo
- GitHub 账号
◇安装 Hugo
brew install hugo
确认安装成功:
hugo version
★创建 Hugo 博客
mkdir blog && cd blog
hugo new site .
初始化 Git:
git init
★选择 & 添加主题
推荐 PaperMod
主题:
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
编辑 config.toml
,添加:
theme = "PaperMod"
baseURL = "https://yuuniji.github.io"
★编写博客文章
创建第一篇文章:
hugo new posts/hello-world.md
编辑 content/posts/hello-world.md
:
---
title: "Hello World"
date: 2025-02-22T12:00:00
draft: false
---
这是我的第一篇博客!
★本地预览(第一次部署请跳过)
hugo server -D
在浏览器访问:
http://localhost:1313
★发布到 GitHub
◇1. 创建 GitHub 仓库
blog
(存放 Hugo 源码)yuuniji.github.io
(存放生成的静态文件)
◇2. 推送 blog
仓库
git remote add origin https://github.com/yuuniji/blog.git
git branch -M main
git add .
git commit -m "Initial commit"
git push -u origin main
◇3. 生成静态文件
hugo -D
静态文件位于 public/
目录。
◇4. 推送到 yuuniji.github.io
cd public
git init
git remote add origin https://github.com/yuuniji/yuuniji.github.io.git
git checkout -b main
git add .
git commit -m "Deploy Hugo site"
git push -f origin main
★配置 GitHub Pages
- 进入
yuuniji.github.io
仓库 Settings → Pages。 - 选择
main
分支,保存后等待部署完成。 - 访问
https://yuuniji.github.io
查看博客。
★自动化部署(可选)
◇1. 在 blog
仓库添加 deploy.sh
nano deploy.sh
粘贴以下内容:
#!/bin/bash
hugo -D
cd public
git add .
git commit -m "Deploy: $(date)"
git push origin main
cd ..
保存并退出(按 Ctrl + X
,然后 Y
,再回车)。
◇2. 赋予执行权限
chmod +x deploy.sh
◇3. 运行部署脚本
./deploy.sh
★大功告成!
以后更新博客时,只需:
- 在
blog/
目录下写文章hugo new posts/xxx.md
- 运行
./deploy.sh
- 访问
https://yuuniji.github.io
查看更新
💡 有问题随时复习此笔记! 🚀
★Hugo 博客多语言 & 功能扩展指南
◇快速链接
baseURL = "https://yuuniji.github.io"
title = "叶泽伟的博客"
theme = "PaperMod"
defaultContentLanguage = "zh"
hasCJKLanguage = true
[languages]
[languages.zh]
languageName = "中文"
weight = 1
contentDir = "content/zh"
[languages.zh.params]
archivesTitle = "归档"
archivesDescription = "按年份和月份查看文章归档"
[languages.zh.params.homeInfoParams]
Title = "你好 👋"
Content = "欢迎来到我的博客!这里有技术、日语学习和生活记录。"
[[languages.zh.params.socialIcons]]
name = "portfolio"
url = "https://xxx.github.io/about/"
[[languages.zh.params.socialIcons]]
name = "youtube"
url = "https://www.youtube.com/xxx"
[[languages.zh.params.socialIcons]]
name = "instagram"
url = "https://www.instagram.com/xxx/"
[[languages.zh.params.socialIcons]]
name = "linkedin"
url = "https://www.linkedin.com/in/xxx"
[[languages.zh.params.socialIcons]]
name = "x"
url = "https://x.com/xxx"
[[languages.zh.params.socialIcons]]
name = "github"
url = "https://github.com/xxx"
[languages.zh.menu]
[[languages.zh.menu.main]]
name = "首页"
url = "/"
weight = 1
[[languages.zh.menu.main]]
name = "标签"
url = "/tags/"
weight = 2
[[languages.zh.menu.main]]
name = "归档"
url = "/archives/"
weight = 3
[[languages.zh.menu.main]]
name = "搜索"
url = "/search/"
weight = 4
[languages.en]
languageName = "English"
weight = 2
contentDir = "content/en"
[languages.en.params]
archivesTitle = "Archives"
archivesDescription = "Browse articles by year and month"
[languages.en.params.homeInfoParams]
Title = "Hi there 👋"
Content = "Welcome to my blog! Here you'll find tech content, Japanese learning, and life experiences."
[[languages.en.params.socialIcons]]
name = "portfolio"
url = "https://xxx.github.io/about/"
[[languages.en.params.socialIcons]]
name = "youtube"
url = "https://www.youtube.com/xxx"
[[languages.en.params.socialIcons]]
name = "instagram"
url = "https://www.instagram.com/xxx/"
[[languages.en.params.socialIcons]]
name = "linkedin"
url = "https://www.linkedin.com/in/xxx"
[[languages.en.params.socialIcons]]
name = "x"
url = "https://x.com/xxx"
[[languages.en.params.socialIcons]]
name = "github"
url = "https://github.com/xxx"
[languages.en.menu]
[[languages.en.menu.main]]
name = "Home"
url = "/"
weight = 1
[[languages.en.menu.main]]
name = "Tags"
url = "/tags/"
weight = 2
[[languages.en.menu.main]]
name = "Archives"
url = "/archives/"
weight = 3
[[languages.en.menu.main]]
name = "Search"
url = "/search/"
weight = 4
[languages.ja]
languageName = "日本語"
weight = 3
contentDir = "content/ja"
[languages.ja.params]
archivesTitle = "アーカイブ"
archivesDescription = "年と月ごとに記事を表示"
[languages.ja.params.homeInfoParams]
Title = "こんにちは 👋"
Content = "私のブログへようこそ!ここには技術、日本語学習、生活記録があります。"
[[languages.ja.params.socialIcons]]
name = "portfolio"
url = "https://xxx.github.io/about/"
[[languages.ja.params.socialIcons]]
name = "youtube"
url = "https://www.youtube.com/xxx"
[[languages.ja.params.socialIcons]]
name = "instagram"
url = "https://www.instagram.com/xxx/"
[[languages.ja.params.socialIcons]]
name = "linkedin"
url = "https://www.linkedin.com/in/xxx"
[[languages.ja.params.socialIcons]]
name = "x"
url = "https://x.com/xxx"
[[languages.ja.params.socialIcons]]
name = "github"
url = "https://github.com/xxx"
[languages.ja.menu]
[[languages.ja.menu.main]]
name = "ホーム"
url = "/"
weight = 1
[[languages.ja.menu.main]]
name = "タグ"
url = "/tags/"
weight = 2
[[languages.ja.menu.main]]
name = "アーカイブ"
url = "/archives/"
weight = 3
[[languages.ja.menu.main]]
name = "検索"
url = "/search/"
weight = 4
[outputs]
home = ["HTML", "RSS", "JSON", "SITEMAP"]
section = ["HTML", "RSS"]
page = ["HTML"]
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true # 允许 HTML 解析
[params]
defaultTheme = "auto"
ShowAllPagesInArchive = true
ShowShareButtons = true
ShowReadingTime = true
ShowToc = true
TocOpen = false
ShowBreadCrumbs = true
ShowCodeCopyButtons = true
ShowPostNavLinks = true
ShowRssButtonInSectionTermList = true
archives = true
[params.assets]
favicon = "favicon.ico"
[params.fuseOpts]
isCaseSensitive = false
shouldSort = true
location = 0
distance = 1_000
threshold = 0.4
minMatchCharLength = 0
keys = [ "title", "permalink", "summary", "content" ]
[sitemap]
changeFreq = ''
disable = false
filename = 'sitemap.xml'
priority = -1
[content]
archivesDir = "content/archives"
◇1. 目录结构
按照我的 hugo.toml
配置,我的 content
目录结构应该是:
content/
├── zh/
│ ├── _index.md
│ ├── about.md
│ ├── archives.md
│ ├── search.md
│ ├── categories/_index.md
│ ├── posts/_index.md
│ ├── tags/_index.md
├── en/
│ ├── _index.md
│ ├── about.md
│ ├── archives.md
│ ├── search.md
│ ├── categories/_index.md
│ ├── posts/_index.md
│ ├── tags/_index.md
├── ja/
├── _index.md
├── about.md
├── archives.md
├── search.md
├── categories/_index.md
├── posts/_index.md
├── tags/_index.md
同时,你需要创建 archetypes
目录来适配不同语言:
archetypes/
├── default.md
├── zh.md
├── en.md
├── ja.md
◇2. archetypes
目录配置
每个语言的 archetypes
文件用于设置默认的 front matter。
默认 archetypes/default.md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
---
archetypes/zh.md
(中文)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "zh"
---
archetypes/en.md
(英文)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "en"
---
archetypes/ja.md
(日语)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "ja"
---
◇3. 创建不同语言的文章
使用 --kind
选项指定 archetypes
,并将文章创建到对应的 content/
目录。
中文文章
hugo new content/zh/posts/my-first-post1.md
这会在 content/zh/posts/
目录下创建 my-first-post.md
,并使用 archetypes/zh.md
作为模板。
英文文章
hugo new content/en/posts/my-first-post1.md
这会在 content/en/posts/
目录下创建 my-first-post.md
,并使用 archetypes/en.md
作为模板。
日语文章
hugo new content/ja/posts/my-first-post1.md
这会在 content/ja/posts/
目录下创建 my-first-post.md
,并使用 archetypes/ja.md
作为模板。
自动化创建不同语言的文章
create_multilingual_post.sh 脚本
#!/bin/bash
# 检查传入参数是否存在
if [ -z "$1" ]; then
echo "请输入文章标题。"
exit 1
fi
TITLE="$1"
DATE=$(date +%Y-%m-%d)
# 创建文章文件名(使用短横线分隔)
POST_NAME=$(echo "$TITLE" | tr " " "-")
# 生成中文文章
mkdir -p "content/zh/posts"
cat > "content/zh/posts/$POST_NAME.md" <<EOL
---
title: "$TITLE"
date: $DATE
lang: "zh"
draft: true
tags: []
categories: []
---
这里是中文版本的内容。
EOL
# 生成英文文章
mkdir -p "content/en/posts"
cat > "content/en/posts/$POST_NAME.md" <<EOL
---
title: "$TITLE"
date: $DATE
lang: "en"
draft: true
tags: []
categories: []
---
This is the English version of the post.
EOL
# 生成日语文章
mkdir -p "content/ja/posts"
cat > "content/ja/posts/$POST_NAME.md" <<EOL
---
title: "$TITLE"
date: $DATE
lang: "ja"
draft: true
tags: []
categories: []
---
この記事は日本語バージョンです。
EOL
echo "已成功创建三种语言版本的文章:$TITLE"
在终端中,先切换到脚本所在的目录,并授予执行权限:
chmod +x create_multilingual_post.sh
执行以下命令,并提供文章标题:
./create_multilingual_post.sh "你的文章标题"
◇总结
- 创建不同语言的文章 需要
hugo new --kind <language> <path>
。 archetypes
目录 应该有zh.md
、en.md
、ja.md
以匹配content/
目录。- 保持
contentDir
结构一致,避免 Hugo 生成文章时找不到正确的模板。
这样,你就可以正确管理 Hugo 多语言博客的文章创建了!🚀
★升级
◇添加侧边栏目录
PaperMod 主题本身不支持侧边栏目录,但你可以利用 arashsm79/hugo-PaperMod-Mod 仓库中的 sidetoc.css
文件来实现侧边栏目录。步骤如下:
- 下载
sidetoc.css
文件:sidetoc.css。 - 将文件放置到你的 Hugo 站点的
assets/css/extended/
目录中。如果目录不存在,请创建它。 - 重新构建站点(运行
hugo
命令),目录应出现在文章的侧边。
注意事项
- 确保文章包含足够的标题(如
# H1
、## H2
等),否则目录不会生成。 sidetoc.css
使用 CSS Grid 和position: sticky
实现侧边固定效果,适合大屏幕显示。- 如果外观或位置不满意,可以自定义
sidetoc.css
文件。
解决长 TOC 不可滚动的难题
- 研究表明,目录过长时可以通过设置最大高度和溢出滚动来解决。
- 倾向于在自定义 CSS 中添加
max-height
和overflow-y: auto
。 - PaperMod 官方不直接支持长目录滚动,需依赖社区修改。
实现步骤
以下是详细的实现步骤,确保长目录可滚动:
-
确认 TOC 的 HTML 结构
- 在浏览器中打开文章页面,右键点击目录区域,选择“检查”(Inspect)或“审查元素”。
- 找到包含目录的 HTML 元素,通常是
<nav id="TableOfContents">
。Hugo 的默认 TOC 生成器会创建此 ID。 - 检查是否有其他包裹元素(如
<div class="toc">
或类似),这些可能是sidetoc.css
添加的。如果有,记录类名或 ID 以便后续调整。
-
添加自定义 CSS
- PaperMod 主题支持通过
assets/css/extended/
目录添加自定义 CSS。用户可以创建或编辑此目录下的文件,例如custom.css
。 - 在
custom.css
中添加以下代码:#TableOfContents { max-height: 80vh; /* 最大高度为视口高度的 80%,可根据需要调整 */ overflow-y: auto; /* 垂直方向溢出时显示滚动条 */ }
- 如果 TOC 被包裹在其他容器中(例如
<div class="toc">
),调整选择器为:.toc { max-height: 80vh; overflow-y: auto; }
max-height: 80vh
是建议值,用户可以根据侧边栏布局调整,例如70vh
或500px
,以适应不同屏幕大小。
- PaperMod 主题支持通过
◇名言警句
创建 quoteszh.html、quotesen.html、quotesja.html 文件并放置到你的 Hugo 站点的 layouts/shortcodes/ 目录中,然后在 content/lang/_index.md 中引用即可。
<!-- layouts/shortcodes/quoteszh.html -->
<blockquote>
<p>{{ .Inner }}</p>
<footer>{{ .Get "source" }}</footer>
</blockquote>
<div id="quote" class="quote-box"></div>
<script>
const quotes = [
"",
"",
""
]
// 随机选择一个引语
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
// 显示引语
document.getElementById('quote').innerHTML = randomQuote;
</script>
<style>
.quote-box {
max-width: 600px;
font-size: 0.8rem;
text-align: left;
.quote-box {
animation: fadeIn 1.5s ease-in-out;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
★其他
◇代码边框圆角
在 /assets/css/common/post-single.css
中修改:
.post-content .highlight pre {
background-color: var(--theme) !important;
margin: 0;
}
◇添加多语言(中文、英文、日文)的网站运行时间
在 /layouts/partials/footer.html
中的
<span id="runtime_span"></span> <!-- 用于显示网站运行时间的容器 -->
<script type="text/javascript">
function show_runtime() {
// 每隔 1 秒执行一次 show_runtime,实现实时更新
setTimeout(show_runtime, 1000);
// 设置网站起始运行时间(2025年2月22日 00:00:00)
const startDate = new Date("2025/02/22 00:00:00");
// 获取当前时间
const now = new Date();
// 计算时间差(毫秒)
const diff = now.getTime() - startDate.getTime();
// 计算运行的天、小时、分钟、秒
const days = Math.floor(diff / (24 * 60 * 60 * 1000)); // 天数
const hours = Math.floor((diff % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); // 小时
const minutes = Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000)); // 分钟
const seconds = Math.floor((diff % (60 * 1000)) / 1000); // 秒
// 获取当前 HTML 页面设置的语言(由 Hugo 生成,如 <html lang="ja">)
const lang = document.documentElement.lang || "en"; // 默认英文
// 各语言的显示文本模板
const translations = {
zh: `网站已运行 ${days} 天 ${hours} 小时 ${minutes} 分 ${seconds} 秒`,
ja: `サイトは稼働してから ${days}日 ${hours}時間 ${minutes}分 ${seconds}秒`,
en: `Site has been running for ${days} days ${hours} hours ${minutes} minutes ${seconds} seconds`
};
// 根据当前语言选择显示内容,若找不到则使用英文
const output = translations[lang] || translations["en"];
// 将显示内容写入页面中
document.getElementById("runtime_span").innerHTML = output;
}
// 初始化运行
show_runtime();
</script>
◇评论
好的,下面是为你的 Hugo 博客详细集成 Giscus 评论系统的完整步骤,适用于部署在 GitHub Pages 或其他静态托管平台。
1:准备 GitHub 仓库
-
登录你的 GitHub 账号。
-
打开你的博客仓库(例如
yourname/yourblog
)。 -
确保开启 Discussions 功能:
- 进入仓库设置 → Features → 勾选 Discussions。
- 进入 Discussions 页面,新建一个 Discussion Category(例如
General
)。
2:生成 Giscus 嵌入代码
-
打开 Giscus 配置页面: 👉 https://giscus.app
-
配置如下:
- Repository:你的博客仓库(如
yourname/yourblog
) - Repository ID / Category / Category ID:根据你 Discussions 中的信息自动生成
- Discussion Mapping:建议选择
pathname
- Reaction:开启(👍等)
- Input Position:bottom(评论框在底部)
- Theme:
preferred_color_scheme
(自动适应浅/深色) - Language:选择
zh-CN
(中文),ja
(日语),或en
(英文)
- Repository:你的博客仓库(如
-
复制生成的
<script>
代码。
3:在 Hugo 模板中插入 Giscus 代码
你可以把 Giscus 的代码放进一个单独文件,例如:layouts/partials/giscus.html
<div id="giscus_container"></div>
<script src="https://giscus.app/client.js"
data-repo="yourname/yourblog"
data-repo-id="你的 repo ID"
data-category="General"
data-category-id="你的 category ID"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="zh-CN"
crossorigin="anonymous"
async>
</script>
然后在 single.html
文章模板中调用:
<article>
{{ .Content }}
</article>
{{ partial "giscus.html" . }}
4:重新构建并部署 Hugo 博客
执行:
hugo
◇ Note 的侧边注释功能
1:使用方式示例
这是正文中的一句话重点词继续正文。
这是正文中的一句话<span class="note" data-note="这是边注的内容。">重点词</span>继续正文。
2:添加 CSS 样式
在你的主题或站点的 CSS 文件中添加以下样式(可以放在 assets/css/extended/custom.css
或主题的自定义 CSS 文件中):
.note {
color: #4CAF50;
font-weight: bold;
border-bottom: 1px dotted #4CAF50;
cursor: help;
position: relative;
}
.note:hover::after {
content: attr(data-note);
position: absolute;
bottom: 100%;
left: 0;
background-color: #4CAF50;
color: white;
padding: 5px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1;
}
◇ MarginNote 的侧边注释功能
1:定义 Shortcode(短代码)
在你的 Hugo 项目中创建一个短代码文件:
路径:layouts/shortcodes/marginnote.html
内容如下:
<span class="marginnote" data-note="{{ .Get "note" }}">{{ .Inner }}</span>
使用方式示例:
这是正文中的一句话重点词 继续正文。
2:添加 CSS 样式
在你的主题或站点的 CSS 文件中添加以下样式(可以放在 assets/css/extended/custom.css
或主题的自定义 CSS 文件中):
.marginnote {
position: relative;
cursor: pointer;
color: #007acc;
border-bottom: 1px dotted #007acc;
}
.marginnote::after {
content: attr(data-note);
display: none;
position: absolute;
top: 1.5em;
left: 100%;
width: 220px;
padding: 8px 12px;
margin-left: 10px;
background: #f9f9f9;
border: 1px solid #ccc;
font-size: 0.9em;
z-index: 999;
white-space: normal;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.marginnote:hover::after {
display: block;
}
效果说明
- 当鼠标悬停在正文的关键词上时,边注会在右侧弹出,不会打断读者阅读流程。
- 不影响 Markdown 文章格式,短代码使用方便。
◇图片点击放大功能
方法很多,这里使用Medium Zoom(推荐)
1:创建部分模板文件
在您的Hugo项目中创建文件:layouts/partials/image-zoom.html
<!-- 在 Hugo 的 layouts/partials/ 目录下创建 image-zoom.html 文件 -->
<!-- layouts/partials/image-zoom.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/medium-zoom/1.0.6/medium-zoom.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 为所有文章内容中的图片添加点击放大功能
mediumZoom('article img', {
margin: 24,
background: 'rgba(0, 0, 0, 0.8)',
scrollOffset: 40
});
});
</script>
<!-- 为了使图片看起来可点击,可以添加一些CSS样式 -->
<style>
article img {
cursor: zoom-in;
transition: transform 0.2s ease-in-out;
}
article img:hover {
transform: scale(1.02);
}
</style>
2:在布局模板中包含该部分
将以下代码添加到您的主布局模板文件(通常位于layouts/_default/baseof.html
或layouts/_default/single.html
)中,放在</body>
标签之前:
{{ partial "image-zoom.html" . }}
★Hugo 博客集成 InstantClick 实现无刷新跳转
◇目的
提升博客内文章跳转体验,实现“无刷新秒开”,减少页面闪烁。
◇步骤与修改说明
-
引入 InstantClick 脚本
- 在
themes/PaperMod/layouts/partials/footer.html
文件的{{- partial "extend_footer.html" . }}
之后,插入如下代码:<!-- InstantClick 3.1.0 CDN 脚本集成 --> <script src="https://cdn.jsdelivr.net/npm/instantclick@3.1.0/instantclick.min.js" data-no-instant></script> <script data-no-instant> // 统计脚本适配示例(可根据实际情况调整) document.addEventListener('instantclick:change', function() { // Google Analytics if (typeof ga === 'function') { ga('set', 'page', location.pathname + location.search); ga('send', 'pageview'); } // 百度统计(假设 _hmt 已加载) if (typeof _hmt === 'object' && _hmt.push) { _hmt.push(['_trackPageview', location.pathname + location.search]); } // 你可以在这里添加其它统计或初始化代码 }); InstantClick.init(); </script>
- 这样所有博客内的页面跳转都将变为无刷新,体验更丝滑。
- 在
-
统计脚本适配
- 针对 Google Analytics、百度统计等常见统计,增加了
instantclick:change
事件监听,保证跳转后统计数据正常。
- 针对 Google Analytics、百度统计等常见统计,增加了
-
可随时移除
- 若需恢复原状,只需删除上述代码块即可。
◇注意事项
- 若有其它自定义 JS 或第三方脚本,也可在
instantclick:change
事件中做初始化。 - InstantClick 不影响 SEO,不影响页面首屏加载速度。
★Hugo 博客实现系统深色/浅色模式自动切换(PaperMod)
◇问题背景
- Hugo + PaperMod 主题默认只在页面加载时检测系统深浅色模式(
prefers-color-scheme
),切换系统主题后需要手动刷新页面才能生效,影响体验。
◇实现目标
- 当用户切换操作系统的深色/浅色模式时,博客页面自动同步切换,无需刷新,体验类似现代主流网站。
◇实现步骤
-
在
themes/PaperMod/layouts/partials/footer.html
末尾插入如下 JS 代码:<script> // 自动响应系统深/浅色切换 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { const isDark = e.matches; document.body.classList.toggle('dark', isDark); localStorage.setItem('pref-theme', isDark ? 'dark' : 'light'); // 如有自定义主题切换按钮,也可在此同步状态 }); </script>
-
原理说明
- 监听
prefers-color-scheme: dark
的change
事件,系统主题切换时自动更新页面主题。 - 同步更新
localStorage
,保证页面刷新后仍保持用户选择。
- 监听
-
注意事项
- 如果你有主题切换按钮,确保按钮状态与自动切换逻辑兼容。
- 推荐将这段代码放在所有
<script>
的最后,保证优先级和兼容性。
◇效果
- 切换系统主题(如 macOS、Windows、移动端等)时,博客页面会立即自动切换,无需刷新,提升用户体验。
★音乐播放器
◇实现目标
- 固定在右下角的迷你播放器
- 展开/折叠播放列表
- 播放/暂停、上一首、下一首控制按钮
- 保留切换文章时音乐继续播放的状态(通过 localStorage)
- 音乐播放器毛玻璃背景设计
◇实现步骤
- 创建文件 layouts/partials/musicplayer.html::
<!-- layouts/partials/musicplayer.html -->
<style scoped>
.music-player-container {
position: fixed;
bottom: 12px;
right: 12px;
z-index: 9999;
font-family: system-ui, sans-serif;
font-size: 13px;
}
.music-player-container .music-panel {
display: none;
margin-bottom: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px 14px;
width: 200px;
max-height: 180px;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
}
.music-player-container .music-panel.show {
display: block;
}
.music-player-container .music-title {
font-size: 13px;
color: var(--primary, #333);
font-weight: 500;
margin-bottom: 6px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.music-player-container .playlist {
list-style: none;
padding: 0;
margin: 0;
}
.music-player-container .playlist li {
padding: 4px 6px;
border-radius: 6px;
cursor: pointer;
color: var(--content, #666);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.music-player-container .playlist li:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.music-player-container .playlist li.active {
background: rgba(255, 255, 255, 0.2);
color: var(--accent, #007acc);
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.music-player-container .mini-player {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 100px;
display: flex;
gap: 8px;
padding: 6px 10px;
align-items: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
transition: all 0.3s ease;
}
.music-player-container .mini-player:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.music-player-container .mini-player button {
background: none;
border: none;
font-size: 16px;
color: var(--secondary, #888);
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: all 0.2s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.music-player-container .mini-player button:hover {
color: var(--accent, #007acc);
background: rgba(255, 255, 255, 0.1);
transform: scale(1.1);
}
.music-player-container .mini-player button:active {
color: var(--accent-active, #005a99);
transform: scale(0.95);
}
.music-player-container audio {
display: none;
}
/* 滚动条美化 */
.music-player-container .music-panel::-webkit-scrollbar {
width: 4px;
}
.music-player-container .music-panel::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.music-player-container .music-panel::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.music-player-container .music-panel::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
<div class="music-player-container" id="musicPlayerContainer">
<div class="music-panel" id="musicPanel">
<div class="music-title" id="musicTitle">Loading...</div>
<ul class="playlist" id="musicPlaylist"></ul>
</div>
<div class="mini-player" id="miniPlayer">
<button id="musicTogglePanel" title="Toggle Playlist">📂</button>
<button id="musicPrevBtn" title="Previous">⏮️</button>
<button id="musicPlayBtn" title="Play">▶️</button>
<button id="musicNextBtn" title="Next">⏭️</button>
</div>
<audio id="musicAudio"></audio>
</div>
<script>
(function() {
'use strict';
// 命名空间前缀,避免全局冲突
const MUSIC_PLAYER_NS = 'HugoMusicPlayer_';
const base = "https://yuuniji.github.io/music/lofi_beats/";
const jsonURL = base + "songs.json";
const STORAGE_KEY = MUSIC_PLAYER_NS + "state";
const INTERACT_KEY = MUSIC_PLAYER_NS + "user_interacted";
async function initMusicPanel() {
try {
const res = await fetch(jsonURL);
const songs = await res.json();
if (!songs.length) return;
let currentIndex = 0;
let isPlaying = false;
// 使用带命名空间的ID选择器
const audio = document.getElementById("musicAudio");
const playBtn = document.getElementById("musicPlayBtn");
const prevBtn = document.getElementById("musicPrevBtn");
const nextBtn = document.getElementById("musicNextBtn");
const toggleBtn = document.getElementById("musicTogglePanel");
const panel = document.getElementById("musicPanel");
const title = document.getElementById("musicTitle");
const playlist = document.getElementById("musicPlaylist");
// 检查元素是否存在
if (!audio || !playBtn || !prevBtn || !nextBtn || !toggleBtn || !panel || !title || !playlist) {
console.warn('Music player elements not found');
return;
}
// 恢复保存的状态
const savedStateStr = localStorage.getItem(STORAGE_KEY);
if (savedStateStr) {
try {
const saved = JSON.parse(savedStateStr);
if (saved.index >= 0 && saved.index < songs.length) {
currentIndex = saved.index;
audio.currentTime = saved.time || 0;
isPlaying = saved.isPlaying || false;
}
} catch (e) {
console.warn('Failed to parse saved music player state:', e);
}
}
// 构建播放列表
songs.forEach((song, index) => {
const li = document.createElement("li");
li.textContent = song.title;
li.onclick = function() {
currentIndex = index;
loadAndPlay(currentIndex);
localStorage.setItem(INTERACT_KEY, "true");
};
playlist.appendChild(li);
});
function highlight(index) {
const listItems = playlist.querySelectorAll("li");
listItems.forEach((li, idx) => {
li.classList.toggle("active", idx === index);
});
}
function loadAndPlay(index) {
audio.src = base + songs[index].file;
audio.currentTime = 0;
audio.play().then(() => {
isPlaying = true;
playBtn.textContent = "⏸️";
title.textContent = songs[index].title;
highlight(index);
saveState();
}).catch((error) => {
console.warn('Failed to play audio:', error);
isPlaying = false;
playBtn.textContent = "▶️";
});
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
index: currentIndex,
time: audio.currentTime,
isPlaying: !audio.paused
}));
} catch (e) {
console.warn('Failed to save music player state:', e);
}
}
// 初始化显示
audio.src = base + songs[currentIndex].file;
title.textContent = songs[currentIndex].title;
highlight(currentIndex);
// 更新播放按钮状态的函数
function updatePlayButton() {
if (audio.paused) {
playBtn.textContent = "▶️";
isPlaying = false;
} else {
playBtn.textContent = "⏸️";
isPlaying = true;
}
}
// 事件监听器
playBtn.onclick = function() {
if (audio.paused) {
audio.play().then(() => {
updatePlayButton();
saveState();
}).catch((error) => {
console.warn('Failed to play audio:', error);
updatePlayButton();
});
} else {
audio.pause();
updatePlayButton();
saveState();
}
};
prevBtn.onclick = function() {
currentIndex = (currentIndex - 1 + songs.length) % songs.length;
loadAndPlay(currentIndex);
};
nextBtn.onclick = function() {
currentIndex = (currentIndex + 1) % songs.length;
loadAndPlay(currentIndex);
};
toggleBtn.onclick = function() {
panel.classList.toggle("show");
};
// 音频事件监听
audio.ontimeupdate = saveState;
audio.onplay = function() {
updatePlayButton();
saveState();
};
audio.onpause = function() {
updatePlayButton();
saveState();
};
audio.onended = function() {
currentIndex = (currentIndex + 1) % songs.length;
loadAndPlay(currentIndex);
};
// 页面加载完成后尝试自动播放
if (document.readyState === 'complete') {
tryAutoPlay();
} else {
window.addEventListener("load", tryAutoPlay);
}
function tryAutoPlay() {
audio.play().then(() => {
updatePlayButton();
saveState();
}).catch(() => {
updatePlayButton();
});
}
} catch (error) {
console.error('Failed to initialize music player:', error);
}
}
// 确保DOM加载完成后再初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMusicPanel);
} else {
initMusicPanel();
}
})();
</script>
- 在布局模板中包含该部分
将以下代码添加到您的主布局模板文件(通常位于layouts/_default/baseof.html
或layouts/_default/single.html
)中,放在</body>
标签之前:
{{ partial "musicplayer.html" . }}