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:

  1. Write posts in blog/ directory: hugo new posts/xxx.md
  2. Run ./deploy.sh
  3. 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 have zh.md, en.md, ja.md to match the content/ 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:

  1. Download the sidetoc.css file: sidetoc.css.
  2. Place the file in your Hugo site’s assets/css/extended/ directory. Create the directory if it does not exist.
  3. 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 and position: 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 and overflow-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:

  1. 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 by sidetoc.css. Note their class or ID for later adjustments.
  2. Add Custom CSS

    • PaperMod supports custom CSS through the assets/css/extended/ directory. Create or edit a file in this directory, such as custom.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., to 70vh or 500px) based on your sidebar layout and screen size requirements.

◇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

  1. Log in to your GitHub account.
  2. Navigate to your blog repository (e.g., yourname/yourblog).
  3. Enable the Discussions feature:
    • Go to SettingsFeatures → Check Discussions.
    • Visit the Discussions tab and create a new Discussion Category (e.g., General).

2: Generate Giscus Embed Code

  1. Visit the Giscus configuration page: 👉 https://giscus.app

  2. 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), or en (English)
  3. 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

  1. 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>
    
  2. Explanation of the Logic

    • Listens for the change event of prefers-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.
  3. 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

  1. 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>
  1. 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:

  1. 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 }}
  1. 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 }}
  1. Explanation
  • Uses display: flex and justify-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.