Hugo Blog + GitHub Pages Complete Deployment Guide
★Environment Preparation
- Device: MacBook (macOS)
- Tools:
- Git
- Hugo
- GitHub Account
◇Install Hugo
brew install hugo
Verify installation:
hugo version
★Create Hugo Blog
mkdir blog && cd blog
hugo new site .
Initialize Git:
git init
★Choose & Add Theme
Recommended PaperMod
theme:
git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
Edit config.toml
, add:
theme = "PaperMod"
baseURL = "https://yuuniji.github.io"
★Write Blog Posts
Create your first post:
hugo new posts/hello-world.md
Edit content/posts/hello-world.md
:
---
title: "Hello World"
date: 2025-02-22T12:00:00
draft: false
---
This is my first blog post!
★Local Preview (Skip for First Deployment)
hugo server -D
Access in browser:
http://localhost:1313
★Publish to GitHub
◇1. Create GitHub Repositories
blog
(for Hugo source code)yuuniji.github.io
(for generated static files)
◇2. Push to blog
Repository
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. Generate Static Files
hugo -D
Static files are located in the public/
directory.
◇4. Push to 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
★Configure GitHub Pages
- Go to
yuuniji.github.io
repository Settings → Pages. - Select
main
branch, save and wait for deployment to complete. - Visit
https://yuuniji.github.io
to view your blog.
★Automated Deployment (Optional)
◇1. Add deploy.sh
to blog
Repository
nano deploy.sh
Paste the following content:
#!/bin/bash
hugo -D
cd public
git add .
git commit -m "Deploy: $(date)"
git push origin main
cd ..
Save and exit (press Ctrl + X
, then Y
, and Enter).
◇2. Grant Execution Permission
chmod +x deploy.sh
◇3. Run Deployment Script
./deploy.sh
★All Done!
To update your blog in the future:
- Write posts in
blog/
directory:hugo new posts/xxx.md
- Run
./deploy.sh
- Visit
https://yuuniji.github.io
to see updates
💡 Refer back to this guide if you have any questions! 🚀
★Hugo Blog Multi-language & Feature Expansion Guide
◇Quick Links
baseURL = "https://yuuniji.github.io"
title = "Yuuniji的博客"
theme = "PaperMod"
defaultContentLanguage = "zh"
hasCJKLanguage = true
[languages]
[languages.zh]
languageName = "中文"
weight = 1
contentDir = "content/zh"
[languages.zh.params]
archivesTitle = "归档"
archivesDescription = "按年份和月份查看文章归档"
[languages.zh.menu]
[[languages.zh.menu.main]]
name = "首页"
url = "/"
weight = 1
[[languages.zh.menu.main]]
name = "关于"
url = "/about/"
weight = 2
[[languages.zh.menu.main]]
name = "标签"
url = "/tags/"
weight = 3
[[languages.zh.menu.main]]
name = "归档"
url = "/archives/"
weight = 4
[[languages.zh.menu.main]]
name = "搜索"
url = "/search/"
weight = 5
[languages.en]
languageName = "English"
weight = 2
contentDir = "content/en"
[languages.en.params]
archivesTitle = "Archives"
archivesDescription = "Browse articles by year and month"
[languages.en.menu]
[[languages.en.menu.main]]
name = "Home"
url = "/"
weight = 1
[[languages.en.menu.main]]
name = "About"
url = "/about/"
weight = 2
[[languages.en.menu.main]]
name = "Tags"
url = "/tags/"
weight = 3
[[languages.en.menu.main]]
name = "Archives"
url = "/archives/"
weight = 4
[[languages.en.menu.main]]
name = "Search"
url = "/search/"
weight = 5
[languages.ja]
languageName = "日本語"
weight = 3
contentDir = "content/ja"
[languages.ja.params]
archivesTitle = "アーカイブ"
archivesDescription = "年と月ごとに記事を表示"
[languages.ja.menu]
[[languages.ja.menu.main]]
name = "ホーム"
url = "/"
weight = 1
[[languages.ja.menu.main]]
name = "について"
url = "/about/"
weight = 2
[[languages.ja.menu.main]]
name = "タグ"
url = "/tags/"
weight = 3
[[languages.ja.menu.main]]
name = "アーカイブ"
url = "/archives/"
weight = 4
[[languages.ja.menu.main]]
name = "検索"
url = "/search/"
weight = 5
[outputs]
home = ["HTML", "RSS", "JSON", "SITEMAP"]
section = ["HTML", "RSS"]
archives = ["HTML"]
search = ["HTML"]
[params]
author = "Yuuniji"
defaultTheme = "auto"
ShowAllPagesInArchive = true
ShowShareButtons = true
ShowReadingTime = true
ShowToc = true
TocOpen = false
ShowBreadCrumbs = true
ShowCodeCopyButtons = true
ShowPostNavLinks = true
ShowRssButtonInSectionTermList = true
archives = true
[params.fuseOpts]
isCaseSensitive = false
shouldSort = true
location = 0
distance = 1_000
threshold = 0.4
minMatchCharLength = 0
keys = [ "title", "permalink", "summary", "content" ]
[content]
archivesDir = "content/archives"
◇1. Directory Structure
According to your hugo.toml
configuration, your content
directory structure should be:
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
Additionally, you need to create an archetypes
directory to adapt to different languages:
archetypes/
├── default.md
├── zh.md
├── en.md
├── ja.md
◇2. archetypes
Directory Configuration
Each language’s archetypes
file is used to set default front matter.
Default archetypes/default.md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
---
archetypes/zh.md
(Chinese)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "zh"
---
archetypes/en.md
(English)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "en"
---
archetypes/ja.md
(Japanese)
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
lang: "ja"
---
◇3. Create Posts in Different Languages
Use the --kind
option to specify archetypes
and create posts in the corresponding content/
directory.
Chinese Post
hugo new content/zh/posts/my-first-post1.md
This will create my-first-post.md
in the content/zh/posts/
directory, using archetypes/zh.md
as the template.
English Post
hugo new content/en/posts/my-first-post1.md
This will create my-first-post.md
in the content/en/posts/
directory, using archetypes/en.md
as the template.
Japanese Post
hugo new content/ja/posts/my-first-post1.md
This will create my-first-post.md
in the content/ja/posts/
directory, using archetypes/ja.md
as the template.
Automating the Creation of Articles in Different Languages
create_multilingual_post.sh Script
#!/bin/bash
# Check if the input parameter exists
if [ -z "$1" ]; then
echo "Please enter the article title."
exit 1
fi
TITLE="$1"
DATE=$(date +%Y-%m-%d)
# Create the article file name (using hyphens to separate words)
POST_NAME=$(echo "$TITLE" | tr " " "-")
# Generate the Chinese article
mkdir -p "content/zh/posts"
cat > "content/zh/posts/$POST_NAME.md" <<EOL
---
title: "$TITLE"
date: $DATE
lang: "zh"
draft: true
tags: []
categories: []
---
This is the Chinese version of the content.
EOL
# Generate the English article
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
# Generate the Japanese article
mkdir -p "content/ja/posts"
cat > "content/ja/posts/$POST_NAME.md" <<EOL
---
title: "$TITLE"
date: $DATE
lang: "ja"
draft: true
tags: []
categories: []
---
This is the Japanese version of the article.
EOL
echo "Successfully created articles in three languages: $TITLE"
In the terminal, first navigate to the directory where the script is located and grant execution permissions:
chmod +x create_multilingual_post.sh
Execute the following command and provide the article title:
./create_multilingual_post.sh "Your Article Title"
◇Summary
- Creating posts in different languages requires
hugo new --kind <language> <path>
. archetypes
directory should havezh.md
,en.md
,ja.md
to match thecontent/
directory.- Keep
contentDir
structure consistent to avoid Hugo failing to find the correct template when generating posts.
Now you can correctly manage the creation of posts for a multi-language Hugo blog! 🚀
★Upgrades
◇Adding a Sidebar Table of Contents (TOC)
The PaperMod theme does not natively support a sidebar TOC, but you can implement one using the sidetoc.css
file from the arashsm79/hugo-PaperMod-Mod repository. Follow these steps:
- Download the
sidetoc.css
file: sidetoc.css. - Place the file in your Hugo site’s
assets/css/extended/
directory. Create the directory if it does not exist. - Rebuild your site (run the
hugo
command), and the TOC should appear on the sidebar of your articles.
Notes
- Ensure your articles contain sufficient headings (e.g.,
# H1
,## H2
, etc.), or the TOC will not be generated. sidetoc.css
uses CSS Grid andposition: sticky
to achieve a fixed sidebar effect, suitable for large screens.- If the appearance or positioning is unsatisfactory, you can customize the
sidetoc.css
file.
Addressing the Issue of Non-Scrollable Long TOCs
- Research indicates that overly long TOCs can be managed by setting a maximum height and enabling overflow scrolling.
- It is recommended to add
max-height
andoverflow-y: auto
in custom CSS. - PaperMod does not natively support scrolling for long TOCs, so community modifications are required.
Implementation Steps
Here are the detailed steps to ensure a long TOC is scrollable:
-
Verify the TOC’s HTML Structure
- Open an article page in your browser, right-click the TOC area, and select “Inspect” or “Examine Element.”
- Locate the HTML element containing the TOC, typically
<nav id="TableOfContents">
. Hugo’s default TOC generator creates this ID. - Check for any wrapper elements (e.g.,
<div class="toc">
or similar) that may have been added bysidetoc.css
. Note their class or ID for later adjustments.
-
Add Custom CSS
- PaperMod supports custom CSS through the
assets/css/extended/
directory. Create or edit a file in this directory, such ascustom.css
. - Add the following code to
custom.css
:#TableOfContents { max-height: 80vh; /* Maximum height set to 80% of viewport height, adjustable as needed */ overflow-y: auto; /* Display scrollbar for vertical overflow */ }
- If the TOC is wrapped in another container (e.g.,
<div class="toc">
), adjust the selector as follows:.toc { max-height: 80vh; overflow-y: auto; }
- The
max-height: 80vh
is a suggested value. Adjust it (e.g., to70vh
or500px
) based on your sidebar layout and screen size requirements.
- PaperMod supports custom CSS through the
◇Inspirational Quotes
Create quoteszh.html
, quotesen.html
, and quotesja.html
files and place them in your Hugo site’s layouts/shortcodes/
directory. Then, reference them in 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 = [
"",
"",
""
]
// Randomly select a quote
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
// Display the quote
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>
★Miscellaneous
◇Code Block Rounded Corners
Modify the following in /assets/css/common/post-single.css
:
.post-content .highlight pre {
background-color: var(--theme) !important;
margin: 0;
border-radius: 8px; /* Added for rounded corners */
}
◇Add Multilingual (Chinese, English, Japanese) Website Uptime Display
Add the following code to the <footer>
section in /layouts/partials/footer.html
:
<span id="runtime_span"></span> <!-- Container for displaying website uptime -->
<script type="text/javascript">
function show_runtime() {
// Update every 1 second for real-time display
setTimeout(show_runtime, 1000);
// Set the website start date (February 22, 2025, 00:00:00)
const startDate = new Date("2025/02/22 00:00:00");
// Get the current time
const now = new Date();
// Calculate the time difference (in milliseconds)
const diff = now.getTime() - startDate.getTime();
// Calculate days, hours, minutes, and seconds
const days = Math.floor(diff / (24 * 60 * 60 * 1000)); // Days
const hours = Math.floor((diff % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); // Hours
const minutes = Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000)); // Minutes
const seconds = Math.floor((diff % (60 * 1000)) / 1000); // Seconds
// Get the language set in the HTML page (e.g., <html lang="ja">)
const lang = document.documentElement.lang || "en"; // Default to English
// Translation templates for each language
const translations = {
zh: `Website has been running for ${days} days ${hours} hours ${minutes} minutes ${seconds} seconds`,
ja: `サイトは稼働してから ${days}日 ${hours}時間 ${minutes}分 ${seconds}秒`,
en: `Site has been running for ${days} days ${hours} hours ${minutes} minutes ${seconds} seconds`
};
// Select display content based on the current language, fallback to English
const output = translations[lang] || translations["en"];
// Update the page content
document.getElementById("runtime_span").innerHTML = output;
}
// Initialize the function
show_runtime();
</script>
◇Comments
Below are the detailed steps to integrate the Giscus comment system into your Hugo blog, suitable for deployment on GitHub Pages or other static hosting platforms.
1: Prepare Your GitHub Repository
- Log in to your GitHub account.
- Navigate to your blog repository (e.g.,
yourname/yourblog
). - Enable the Discussions feature:
- Go to Settings → Features → Check Discussions.
- Visit the Discussions tab and create a new Discussion Category (e.g.,
General
).
2: Generate Giscus Embed Code
-
Visit the Giscus configuration page: 👉 https://giscus.app
-
Configure the following:
- Repository: Your blog repository (e.g.,
yourname/yourblog
) - Repository ID / Category / Category ID: Auto-generated based on your Discussions settings
- Discussion Mapping: Recommended to select
pathname
- Reactions: Enable (e.g., 👍)
- Input Position:
bottom
(comment box at the bottom) - Theme:
preferred_color_scheme
(adapts to light/dark mode) - Language: Choose
zh-CN
(Chinese),ja
(Japanese), oren
(English)
- Repository: Your blog repository (e.g.,
-
Copy the generated
<script>
code.
3: Insert Giscus Code into Hugo Template
Create a partial file, e.g., layouts/partials/giscus.html
, with the following content:
<div id="giscus_container"></div>
<script src="https://giscus.app/client.js"
data-repo="yourname/yourblog"
data-repo-id="your repo ID"
data-category="General"
data-category-id="your 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>
Then, include it in the single.html
article template:
<article>
{{ .Content }}
</article>
{{ partial "giscus.html" . }}
4: Rebuild and Deploy Your Hugo Blog
Run the following command:
hugo
Deploy the generated files to your hosting platform.
◇Note Sidebar Annotation Feature
1: Usage example in Markdown
This is a sentence in the main text key term continuing the main text.
This is a sentence in the main text <span class="note" data-note="This is the margin note content.">key term</span> continuing the main text.
2: Add CSS Styles
Add the following styles to your theme or site’s CSS file (e.g., assets/css/extended/custom.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 Sidebar Annotation Feature
1: Define a Shortcode
Create a shortcode file in your Hugo project:
Path: layouts/shortcodes/marginnote.html
Content:
<span class="marginnote" data-note="{{ .Get "note" }}">{{ .Inner }}</span>
Usage example in Markdown:
This is a sentence in the main text key term
continuing the main text.
2: Add CSS Styles
Add the following styles to your theme or site’s CSS file (e.g., assets/css/extended/custom.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;
}
Effect Description
- When hovering over a key term in the main text, the margin note pops up on the right without disrupting the reading flow.
- The shortcode is easy to use and does not affect the Markdown article format.
◇Image Click-to-Zoom Feature
There are several methods; here, we recommend using Medium Zoom.
1: Create a Partial Template File
Create a file in your Hugo project: layouts/partials/image-zoom.html
Content:
<!-- 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() {
// Add click-to-zoom functionality to all images in article content
mediumZoom('article img', {
margin: 24,
background: 'rgba(0, 0, 0, 0.8)',
scrollOffset: 40
});
});
</script>
<!-- Add CSS styles to make images appear clickable -->
<style>
article img {
cursor: zoom-in;
transition: transform 0.2s ease-in-out;
}
article img:hover {
transform: scale(1.02);
}
</style>
2: Include the Partial in the Layout Template
Add the following code to your main layout template (e.g., layouts/_default/baseof.html
or layouts/_default/single.html
) before the </body>
tag:
{{ partial "image-zoom.html" . }}
★Implementing Automatic Dark/Light Mode Switching for Hugo Blog (PaperMod)
◇Problem Background
- By default, Hugo with the PaperMod theme only detects the system’s dark/light mode (
prefers-color-scheme
) when the page loads. Switching the system theme requires manually refreshing the page to take effect, which impacts the user experience.
◇Goal
- Enable the blog page to automatically sync with the system’s dark/light mode when the user switches the operating system theme, without requiring a page refresh, providing an experience similar to modern mainstream websites.
◇Implementation Steps
-
Insert the following JavaScript code at the end of
themes/PaperMod/layouts/partials/footer.html
:<script> // Automatically respond to system dark/light mode changes 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'); // If you have a custom theme toggle button, you can sync its state here }); </script>
-
Explanation of the Logic
- Listens for the
change
event ofprefers-color-scheme: dark
to automatically update the page theme when the system theme changes. - Updates
localStorage
to ensure the user’s theme preference persists after a page refresh.
- Listens for the
-
Notes
- If you have a theme toggle button, ensure its state is compatible with the automatic switching logic.
- It’s recommended to place this code after all other
<script>
tags to ensure proper priority and compatibility.
◇Result
- When switching the system theme (e.g., on macOS, Windows, or mobile devices), the blog page automatically switches themes instantly without requiring a refresh, enhancing the user experience.
★ Music Player
◇ Objectives
- A mini player fixed to the bottom-right corner of the screen
- Expandable/collapsible playlist
- Control buttons for play/pause, previous, and next
- Preserve playback state when switching between articles (using
localStorage
) - Frosted glass background design for the music player
◇ Implementation Steps
- Create the file
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>
- Include the partial in your layout template
Add the following code to your main layout template file (usually located at layouts/_default/baseof.html
or layouts/_default/single.html
), right before the closing </body>
tag:
{{ partial "musicplayer.html" . }}
★icon
https://favicon.io/favicon-generator/
blog/
├── static/
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
★ Pagination Settings
You provided the article list template for PaperMod (typically layouts/_default/list.html
or the main
block within a similar structure). This template already supports pagination with page numbers at the bottom, but by default, it only displays “Previous Page / Next Page” buttons. If you want to explicitly show page numbers at the bottom, modify the following section:
- Locate this code (as you already have):
{{- if gt $paginator.TotalPages 1 }}
<footer class="page-footer">
<nav class="pagination">
{{- if $paginator.HasPrev }}
<a class="prev" href="{{ $paginator.Prev.URL | absURL }}">
« {{ i18n "prev_page" }}
{{- if (.Param "ShowPageNums") }}
{{- sub $paginator.PageNumber 1 }}/{{ $paginator.TotalPages }}
{{- end }}
</a>
{{- end }}
{{- if $paginator.HasNext }}
<a class="next" href="{{ $paginator.Next.URL | absURL }}">
{{- i18n "next_page" }}
{{- if (.Param "ShowPageNums") }}
{{- add 1 $paginator.PageNumber }}/{{ $paginator.TotalPages }}
{{- end }} »
</a>
{{- end }}
</nav>
</footer>
{{- end }}
- Replace with:
{{ if gt .Paginator.TotalPages 1 }}
<footer>
<nav style="display:flex; justify-content:space-between; align-items:center;">
<!-- Left: Previous Page -->
<div>
{{ if .Paginator.HasPrev }}
<a href="{{ .Paginator.Prev.URL | absURL }}">« Previous Page</a>
{{ end }}
</div>
<!-- Center: Page Numbers -->
<div>
{{ range .Paginator.Pagers }}
{{ if eq .PageNumber $.Paginator.PageNumber }}
<strong>{{ .PageNumber }}</strong>
{{ else }}
<a href="{{ .URL | absURL }}">{{ .PageNumber }}</a>
{{ end }}
{{ end }}
</div>
<!-- Right: Next Page -->
<div>
{{ if .Paginator.HasNext }}
<a href="{{ .Paginator.Next.URL | absURL }}">Next Page »</a>
{{ end }}
</div>
</nav>
</footer>
{{ end }}
- Explanation
- Uses
display: flex
andjustify-content: space-between
to distribute three sections. - “Previous Page” on the left, “Next Page” on the right, and page numbers in the center.
- Page numbers are styled minimally with
<a>
and<strong>
tags, with the current page number bolded. - No additional CSS file is required; inline styles are sufficient.