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
、Enter)。
◇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 = "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. ディレクトリ構造
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 "3つの言語バージョンの記事が正常に作成されました:$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
ファイルをカスタマイズできます。
長い目次がスクロールできない問題の解決
- 調査によると、目次が長すぎる場合は、最大高さを設定し、スクロール可能なオーバーフローを有効にすることで解決できます。
- カスタムCSSに
max-height
とoverflow-y: auto
を追加することを推奨します。 - PaperMod公式では長い目次のスクロールを直接サポートしていないため、コミュニティの修正に依存する必要があります。
実装手順
長い目次をスクロール可能にするための詳細な手順は以下の通りです:
-
目次のHTML構造を確認
- ブラウザで記事ページを開き、目次エリアを右クリックして「検証」(Inspect)または「要素を調査」を選択します。
- 目次を含むHTML要素を見つけます。通常は
<nav id="TableOfContents">
です。Hugoのデフォルト目次ジェネレーターはこの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; /* 垂直方向のオーバーフロー時にスクロールバーを表示 */ }
- 目次が他のコンテナ(例:
<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;
border-radius: 8px; /* 角丸を追加 */
}
◇多言語(中国語、英語、日本語)対応のウェブサイト稼働時間表示
/layouts/partials/footer.html
の <footer>
内に以下のコードを追加します:
<span id="runtime_span"></span> <!-- ウェブサイト稼働時間を表示するコンテナ -->
<script type="text/javascript">
function show_runtime() {
// 1秒ごとに実行し、リアルタイム更新を実現
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>
◇コメントシステム
以下は、GitHub Pages やその他の静的ホスティングプラットフォームにデプロイされた Hugo ブログに Giscus コメントシステム を詳細に統合する手順です。
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="リポジトリID"
data-category="General"
data-category-id="カテゴリ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="ja"
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
):
.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:ショートコードの定義
Hugo プロジェクト内にショートコードファイルを作成します:
パス:layouts/shortcodes/marginnote.html
内容:
<span class="marginnote" data-note="{{ .Get "note" }}">{{ .Inner }}</span>
使用例:
これは本文中の文です。強調語 本文を続けます。
2:CSS スタイルの追加
テーマまたはサイトの CSS ファイルに以下のスタイルを追加します(例: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;
}
効果の説明
- マウスを本文のキーワードにホバーすると、サイドノートが右側にポップアップ表示され、読者の閲覧フローを妨げません。
- Markdown の記事形式に影響を与えず、ショートコードは使いやすいです。
◇画像クリック拡大機能
多くの方法がありますが、ここでは Medium Zoom(推奨)を使用します。
1:部分テンプレートファイルの作成
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" . }}