markdownで記事を作成して無料ブログをつくる[Github Pages+Hugo]

HugoとGitHub Pagesで無料ブログを構築する方法を解説。インストールからテーマ設定、Google Analytics連携、サイトマップ作成まで紹介します。

はじめに

GitHub Pagesを用いると、無料でWebサイトを公開できます。静的サイトジェネレータ(今回はHugoを利用)と組み合わせることでブログを簡単に作れることを知り、試してみたくなりました。そのまとめです。GitHub Pagesと静的サイトジェネレータに関する非常に分かりやすい記事が多数ありましたが、自分用のまとめとして作成しました。

全体通して,参考にした記事たち 参考1: Hugo + GitHub Pages(独自ドメイン適応)でサイトを作成・公開する
参考2: 【2018年版】Hugoとgithub pagesでブログ作る方法【Circle CIも回します】

Hugoとは

ブログを作成する際,コンテンツの作成や編集のしやすさからWordPressのようなCMSを利用することが多い.しかし,CMSを使用するにはCMS自体のインストールや設定,データベースの準備など少し複雑な作業が必要となる.小規模なブログの場合,HTMLファイルを用いた静的なサイトを作成したほうがコストが低くなる場合がある.とはいえ,HTMLファイルをいちいち手動で作成するのはめんどくさい.そのため,HTMLファイルを半自動生成するようなツール(静的サイトジェネレータ)が存在し,その一つが Hugo である.

Hugoは,Go言語で開発された静的サイトジェネレータである.Hugoを使うとデータベースを使用することなく静的なHTMLファイルやCSSファイルによるブログを作成できる.コンテンツは,Markdown形式で記述して,ビルドすると静的なHTMLファイルによるウェブサイトが生成される.

Hugoのロゴ

Hugoのメリットしては,

  • ビルド,表示処理が早い
  • データベース不要で管理が簡単

ということがあげられる.

参考1: 静的サイトジェネレータ「Hugo」と技術文書公開向けテーマ「Docsy」でOSSサイトを作る
参考2: Hugoとは?静的サイトジェネレータについて解説

静的サイト生成(SSG)の仕組み:SSR・CSRとの違い

「ビルドすると静的なHTMLファイルが生成される」という一文には,Webサイトのレンダリング方式として重要な意味がある。Webサイトのレンダリング方式は大きく3つに分類できる。

  1. SSG(Static Site Generation):Hugoが採用する方式。コンテンツファイル(Markdown + フロントマター)とレイアウトテンプレートを,Go言語のテンプレートエンジンがビルド時に一度だけ合成し,完成済みのHTMLファイル一式を生成する。生成後は,このHTMLファイルをそのまま配信するだけでよい。
  2. SSR(Server-Side Rendering):Node.jsやRuby on Railsなどのアプリケーションサーバーが,リクエストが来るたびにテンプレートエンジンを実行してHTMLを組み立てる方式。データベースへの問い合わせやテンプレート処理が,アクセス1回ごとに発生する。
  3. CSR(Client-Side Rendering):Reactなどで作られたSPA(Single Page Application)で採用される方式。サーバーはほぼ空のHTMLとJavaScriptバンドルだけを返し,ブラウザ側のJavaScriptエンジンがページを読み込んだ後にレンダリングを行う。

この違いが生む実際上のメリットは2つある。

  • パフォーマンス:SSGでは,テンプレート処理のコストはビルド時に1回支払うだけで済む。訪問者が1人でも10万人でも,配信されるのは事前に生成済みの同じHTMLファイルであり,リクエストのたびにテンプレートエンジンやデータベースを起動する必要がない。SSRのようにアクセスごとにサーバー側の計算コストが発生することはなく,CSRのようにブラウザ側でレンダリングを待つ必要もない。
  • ホスティングの単純さ:ビルド成果物(public/ディレクトリ)は,HTML・CSS・JS・画像だけで構成されたプレーンな静的ファイルである。そのため,アプリケーションサーバーやデータベース,ランタイム環境を一切必要とせず,ファイルを配置できる場所であればどこでもホスティングできる。今回紹介するGitHub Pagesはまさにその典型で,静的ファイルを置くだけの無料ホスティングサービスである。

実際,本ブログ自身のデプロイフロー(.github/workflows/deploy.yml)も,CI上でhugo --gc --minifypush 1回につき1回だけ実行し,生成されたpublic/をそのままGitHub Pagesにデプロイしている。訪問者のアクセスのたびにサーバーが何かを計算する,という工程はどこにも存在しない。

Hugoでサイトを作成

Hugoのインストール

# Homebrew(macOS)の場合
brew install hugo

その他の方法でのインストール方法は, Install Hugo

サイトの作成

hugo new site test # testは好きな名前
cd test
hugo # このコマンドで生成

サーバーでプレビュー

hugo server

http://localhost:1313 で確認できる

フォルダの中身の確認

.
├── archetypes # (初期生成されない)Hugo Pipes で処理するファイルを保存するためのファイル
├── config.toml # hugoの設定ファイル
├── content # 記事ファイルを置くファイル
├── data # サイトの全ページから参照したいデータを記述したファイル
├── layouts # テーマファイルを一部修正したいときやレイアウトパーツを追加したいときに利用するファイル
├── public # 生成されるhtmlなどのコード(これを公開する)
├── static # サイト内の静的ファイルを置くファイル
└── themes # テーマファイルを置く

参考1: HUGO のディレクトリ構成
参考2: Directory Structure

テンプレートを利用する

Hugo Themes から好きなテーマを選択し,themesフォルダの中にcloneする.

cd themes
git clone テーマフォルダ

config.tomlに以下の行を追記する.

theme = "選んだテーマ名"

Hugoテーマの一覧画面

ブログ記事を作成する

cd test
hugo new post/new-post.md

new-post.mdの中身

---
title: "タイトル"
date: 2020-08-16T15:17:23+09:00
draft: false # trueの場合は表示されない
tags: ["python", "ros"]
---
あとは,記事をmarkdownで記載
  • hugoコマンドで、public以下に静的ファイルが生成
cd test
hugo

Github Pages

Githubにリポジトリを作成する

Githubの画面右上のプラスアイコン「+」をクリックし,「New repository」を選択する. フォルダ名は,「アカウント名.github.io」にする. 公開URLは https://アカウント名.github.io となる.

GitHubのリポジトリ作成画面

参考: 自分で作ったWebページをインターネット上に公開しよう!

publicフォルダをここに上げる.

Github Pagesではフォルダ直下のhtmlファイルしか,公開してくれないため,hugoで作成したtestフォルダ内のpublicフォルダを「アカウント名.github.io」にアップロードする. Githubへのアップロード方法は, 初心者用:GitHubにファイルをアップロードする方法(超基本) を参考. testフォルダ自体は,別のリポジトリを作成してそちらで管理する.もしくは,branchで分ける.

その他

hugo 実行方法

ローカルのサーバーで確認

hugo server

ローカルのサーバーで確認(ドラフトも対象)

hugo server -D

latex 数式を使うための編集

/layouts/partials/add_mathjax.htmlを作成

<script>
    MathJax = {
        tex: {
        inlineMath: [['$', '$'], ['\\(', '\\)']]
        }
    };
</script>

<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>

/layouts/partials/site-header.htmlに追記

{{ partial "add_mathjax.html" . }}

参考: MathJaxを用いてHugoで数式を表現する

google Analytics

config.tomlに以下の行を追加

googleAnalytics = "測定ID"

Google Analyticsとの連携のための組み込みテンプレート({{ template “_internal/google_analytics_async.html” . }})が古いため変更を追加

layouts/partials/head.htmlを編集

- {{ template "_internal/google_analytics_async.html" . }}
+ {{- partial "analytics" . -}}

layouts/partials/analytics.htmlを作成

{{ if not .Site.IsServer }}
{{ with .Site.GoogleAnalytics }}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
<script>
   window.dataLayer = window.dataLayer || [];
   function gtag(){dataLayer.push(arguments);}
   gtag('js', new Date());
   gtag('config', '{{ . }}');
</script>
{{ end }}
{{ end }}

参考: Hugoで未だ対応していないgtag.jsを利用して Googleアナリティクスする

Sitemapを作成

layouts/sitemap.xmlを作成

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range .Data.Pages }}{{ if .IsPage }}
<url>
  <loc>{{ .Permalink }}</loc>
  {{ if not .Lastmod.IsZero }}
  <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}</lastmod>
  <changefreq>weekly</changefreq>
  {{ end }}
</url>
{{ end }}{{ end }}
</urlset>

layouts/robots.txtを作成

Sitemap : {{ $.Site.BaseURL }}sitemap.xml

config.tomlを編集

enableRobotsTXT = true

URL /sitemap.xml で表示されます。 https://yuhi-sa.github.io/sitemap.xml

参考: XMLサイトマップの作り方

実行検証:実際にビルドして測ってみる

「ビルドが速い」という主張は,このブログ自身がHugoで動いている実サイトなので,実際にビルドして数値で確認する。

本番サイトでの実測

本ブログ(apps/blog)で本番相当のビルドコマンドを実行した。

time hugo --gc --minify -d /tmp/hugo-build-test-public

結果は以下の通り(コンテンツを一切変更せず,出力先だけを一時ディレクトリに変えた読み取り専用の計測)。

Total in 4674 ms
hugo --gc --minify ...  19.67s user 1.09s system 439% cpu 4.721 total

このビルドは,日本語542ページ・英語563ページ,合計1,105ページ(記事・タグ一覧・ページネーション込み)を,Markdownパース・テンプレート適用・シンタックスハイライト・CSS/JS/HTMLのminify・ガベージコレクションまで含めて,Hugo自身の計測(Total in)で4.674秒,シェルのtimeコマンドでの壁時計時間(プロセス起動〜終了)で4.721秒だった。439% cpuという表示は,マルチコアで並列にレンダリングしていることを示す(ユーザーCPU時間19.67秒を壁時計4.721秒で処理しており,約4.4コア分を使っている計算になる)。

スケーリング検証:ページ数を変えて実測する

本番サイトはテーマ・多言語・アフィリエイト機能などを含むため,「ページ数だけ」の影響を切り分けるのが難しい。そこで,本リポジトリとは完全に独立した一時ディレクトリに,最小限のsingle.html/list.htmlテンプレートだけを持つスクラッチ用Hugoサイトを新規作成し,ダミー記事を10件・100件・1000件に増やしながら実際にビルドして計測した。

生成ページ数(記事+一覧等)ビルド時間(Hugo自己計測)
21(記事10件)12 ms
111(記事100件)24 ms
1,011(記事1000件)154 ms

Hugoのビルド時間はページ数に対してほぼ線形に増加する(実測)

10件から100件(10倍)では12ms→24msと2倍程度の増加に留まるが,これはプロセス起動やモジュール解決など,ページ数に依存しない固定コストが小さなサイトでは支配的なためである。100件から1000件(10倍)では24ms→154msと6.4倍に増加しており,ページ数が大きくなるほどページ処理コストの比率が支配的になる,という直感通りの傾向が確認できた。

一方,本番サイト(1,105ページ,テーマ・多言語・minify込み)は4,674msであり,スクラッチサイトの1,011ページ時点(154ms)と比べて1ページあたりのコストは大幅に高い。これは,実際のテーマではレイアウトの継承・パーシャルの呼び出し・ショートコード展開・シンタックスハイライト・アセットのminifyといった処理が加わるためで,「ページ数」だけでなく「1ページあたりの処理内容」もビルド時間を左右することを示している。それでも1,000ページを超える実サイトが5秒未満でビルドできる点は,Hugoが謳う高速性を裏付ける実測結果と言える。

つまずきやすいポイント(エッジケース)

コンテンツの構成:リーフバンドル・ブランチバンドル・普通のコンテンツファイル

Hugoのコンテンツディレクトリには,3つの異なる構成パターンがあり,画像などの関連ファイルの扱われ方が変わる。スクラッチサイトで実際に検証した。

  1. リーフバンドル(leaf bundle)content/posts/foo/index.mdのように,ディレクトリ内のMarkdownファイルがindex.mdという名前である場合。同じディレクトリに置いた画像ファイルは,テンプレート内で.Resourcesから参照できるページリソースとして扱われる。検証では,index.mdと同じディレクトリに画像を置いたところ,テンプレート側で{{ len .Resources }}1を返した。これにより,{{ with .Resources.GetMatch "fig.png" }}{{ .Resize "300x" }}{{ end }}のような画像のリサイズ・レスポンシブ画像生成といったHugoの組み込み画像処理パイプラインが使える。
  2. ブランチバンドル(branch bundle)content/posts/foo/_index.mdのように,ファイル名が_index.mdである場合。このディレクトリは単体の記事ではなく,セクション(一覧)ページになる。実際に検証すると,_index.mdのあるディレクトリはsingle.htmlではなくlist.htmlテンプレートでレンダリングされ,配下にネストした子ページ(別のリーフバンドルなど)を.Pagesで列挙できた。
  3. 普通のコンテンツファイルindex.mdでも_index.mdでもない名前(例:1.md)のMarkdownファイルを,ディレクトリの中に置くパターン。このブログのcontent/posts/YYYYMMDD_トピック名/1.mdという構成自体がこのパターンであり,今読んでいるこの記事も例外ではない。 このパターンで同様に検証すると,テンプレート側の{{ len .Resources }}0を返した。つまり,リーフバンドルのようなページリソース機能(画像処理など)は使えない。ただし,同じディレクトリに置いた画像ファイル自体はHugoによって出力先の対応するパスへそのままコピーされる(public/posts/20210204_hugo/fig1.pngが実際に生成されることを確認済み)。これが,このブログの記事内で![alt](/posts/20210204_hugo/fig1.png)という絶対パスのMarkdown画像記法がそのまま機能している理由である。裏を返せば,Hugoのビルトイン画像処理(リサイズ・レスポンシブ画像・EXIF読み取りなど)をこれらの画像に使いたい場合は,1.mdindex.mdにリネームしてリーフバンドル化する必要がある——ただしその場合,記事のURLが/posts/20210204_hugo/1/1.md自体がURLセグメントになる,本サイトで実際に使われている構造)から変わってしまう点に注意が必要である。

draft: trueの記事が本番ビルドから消える罠

本記事の「その他」節で紹介したhugo server -Dは,ローカル開発でよく使うコマンドだが,ここに実務でつまずきやすい罠がある。スクラッチサイトでdraft: trueの記事を1件追加し,本番相当のビルド(-Dなし)と,ドラフト込みビルド(-Dあり)を比較した。

=== 本番ビルド(-Dなし)===
Pages: 1011(ドラフト追加前と同じ)
public/posts/draft-test/ → 生成されない

=== -D 付きビルド ===
Pages: 1012(+1)
public/posts/draft-test/index.html → 生成される

hugo server -Dでローカル確認しているときは記事が正しく表示されるため,「動作確認できたから大丈夫」と思い込みやすいが,-Dを付けない本番ビルド(CIが実行するのはまさにこちらのhugo --gc --minify)では,draft: trueの記事はページ数にすら数えられず,出力ファイルも一切生成されない。ローカルで見えていたものが本番で忽然と消える,という典型的な事故につながるため,公開したい記事はdraft: falseになっているか,公開前に必ず-Dなしのビルドでも確認するとよい。

sitemap.xml・RSSフィードの検証

「URL /sitemap.xml で表示されます」と書いたが,実際に本番ビルドの出力を検査して中身を確認した。

  • public/sitemap.xmlは,実は1枚のURLリストではなくサイトマップインデックス<sitemapindex>)だった。本サイトは日本語・英語の多言語構成であり,かつ同一ドメイン配下にNext.js製の別アプリ(DevToolBox・CalcBox・Pomodoro)も同居しているため,https://yuhi-sa.github.io/ja/sitemap.xml.../en/sitemap.xml.../devtoolbox/sitemap.xmlなど,複数のサイトマップへの参照をまとめたインデックスになっている。
  • 言語別のサイトマップの実体はpublic/ja/sitemap.xmlpublic/en/sitemap.xmlにある。中身を見ると,日本語版のURLはhttps://yuhi-sa.github.io/tags/db/のように**/ja/という接頭辞を含まない**実際の配信パスで記載されており,サイトマップファイル自体が/ja/という(実際には存在しない)パスに置かれているにもかかわらず,中のURLは正しく実配信パスを指していることを確認できた。
  • RSSフィード(public/index.xml)も実際に開いて確認したところ,最新記事のタイトル・リンク・公開日時が正しく含まれていた。

「たぶん正しく生成されているはず」で済ませず,実際にビルド出力を開いて確認する習慣が,本番公開後のSEO事故を防ぐ。

Hugoの最近の動向

執筆当時(2021年)から数年が経過しているため,Hugo自体のバージョンと動向を確認した。

  • 本記事執筆にあたり実際に使用したHugoはhugo v0.164.0+extended+withdeploy(2026年7月6日リリース)で,Homebrew経由でインストールしたものである。一方,本ブログのCI(.github/workflows/deploy.yml)が固定しているバージョンは0.155.3であり,ローカルの最新版とは9マイナーバージョンの差がある。
  • v0.164.0では,AVIF形式の画像のエンコード・デコード対応,新しいテンプレート関数(encoding.HexEncodeencoding.HexDecodecrypto.Hash),Pandocの引用文献サポートなどが追加されたほか,v0.128.0から存在していたパフォーマンス上のリグレッション(大規模サイトほど影響が大きい)が修正されている。
  • 実際に本サイトをビルドした際,以下の非推奨警告が出力された:languages.ja.languageNameは Hugo v0.158.0 で非推奨となり,代わりにlanguages.ja.labelを使うべき,.Language.LanguageCode.Language.Localeへ,.Language.LanguageDirection.Language.Directionへ,.Language.LanguageName.Language.Labelへそれぞれ置き換えるようにという内容である。本サイトのconfig.tomlは現時点でこの新しいキーへ未移行であり,将来のメジャーアップデートで削除される前に追従が必要になる。

作成したサイト


関連ツール