gohan — インクリメンタルビルド対応のGo製静的サイトジェネレータの紹介
作った理由
このサイト(bmf-tech.com)はgohanで動いている。自分で完全に理解でき、変更したページだけを再生成する静的サイトジェネレータが欲しかった。ほとんどのジェネレータは無条件全再生成かGit diff依存のどちらかだが、Git diffはブランチ切り替えやフレッシュclone後に信頼性が落ちる。gohanはSHA-256コンテンツハッシングでビルドマニフェストを永続化し、Git履歴に依存せず常に正確な差分ビルドを実現する。
クイックスタート
# 1. プロジェクトディレクトリを作成
mkdir myblog && cd myblog
# 2. config.yaml を追加
cat > config.yaml << 'EOF'
site:
title: My Blog
base_url: https://example.com
language: ja
github_repo: https://github.com/owner/repo # 「このページを編集」リンクに使用
github_branch: main
build:
content_dir: content
output_dir: public
static_dir: static # output_dir のルートにそのままコピーする静的ファイル
per_page: 20 # ページネーション記事数(0 = 無効)
theme:
name: default
syntax_highlight:
theme: github
line_numbers: false
ogp:
enabled: true
width: 1200
height: 630
i18n:
locales: [ja]
default_locale: ja
EOF
# 3. 最初の記事を作成
gohan new --title="Hello, World!" hello-world
# 4. サイトをビルド
gohan build
# 5. ライブリロード付きでプレビュー
gohan serve # http://127.0.0.1:1313 を開く
アーキテクチャ
システム構成
ディレクトリ構造
入力
.
├── config.yaml
├── content/
│ ├── posts/ # ブログ記事(一覧・タグ・アーカイブ対象)
│ └── pages/ # 静的ページ(About, Contact など)
├── themes/
│ └── default/
│ └── layouts/ # テンプレートファイル
├── assets/ # CSS・画像などの静的ファイル
└── taxonomies/
├── tags.yaml
└── categories.yaml
出力
public/
├── index.html
├── posts/
├── pages/
├── tags/
├── categories/
├── archives/
├── feed.xml
├── atom.xml
├── sitemap.xml
└── assets/
インクリメンタルビルドエンジン
インクリメンタルビルドのコアはinternal/diff/git.goにある。Detect()メソッドが現在のワーキングツリーを永続化したBuildManifestと比較する。
func (g *GitDiffEngine) Detect(manifest *model.BuildManifest) (*model.ChangeSet, error) {
current, err := hashAllFiles(g.rootDir)
if err != nil {
return nil, err
}
if manifest == nil {
cs := &model.ChangeSet{}
for path := range current {
cs.AddedFiles = append(cs.AddedFiles, path)
}
return cs, nil
}
cs := &model.ChangeSet{}
for path, hash := range current {
if prev, ok := manifest.FileHashes[path]; !ok {
cs.AddedFiles = append(cs.AddedFiles, path)
} else if prev != hash {
cs.ModifiedFiles = append(cs.ModifiedFiles, path)
}
}
for path := range manifest.FileHashes {
if _, ok := current[path]; !ok {
cs.DeletedFiles = append(cs.DeletedFiles, path)
}
}
return cs, nil
}
hashAllFiles()がコンテンツディレクトリをウォークして全ファイルのSHA-256 hexダイジェストを計算する。初回ビルド(またはマニフェストが存在しない場合)は全ファイルがAddedとみなされる。以降のビルドではAdded、Modified、Deletedの3種類の変更を検出する。影響を受けたHTMLページだけを再生成する。
config.yaml自体もビルドごとにハッシュされており、変更を検知すると自動的にキャッシュをクリアしてフルビルドに切り替わる。--fullフラグで明示的に強制できる。
キャッシュは .gohan/cache/manifest.json に保存される。
.gohan/
└── cache/
└── manifest.json # ファイルハッシュ一覧
ビルドシーケンス(gohan build)
開発サーバー・ライブリロード(gohan serve)
機能一覧
インクリメンタルビルドに加え、gohanは標準で多くの機能を提供する。
- Markdown + Front Matter — GFM (GitHub Flavored Markdown) + YAMLメタデータ対応。
- タクソノミー — タグ・カテゴリーページを自動生成。
- Atomフィード / サイトマップ —
atom.xml・sitemap.xmlを自動生成。 - カスタマイズ可能なテーマ — Go
html/templateによる完全制御。 - i18n —
content/en/とcontent/ja/のようなロケールミラーディレクトリ構造。ロケール切り替えリンクを自動生成。 - シンタックスハイライト — Chromaによるサーバーサイドレンダリング。クライアントサイドJavaScript不要。
- Mermaid図 — ビルド時にSVG変換またはクライアントサイドレンダリング用の
<pre class="mermaid">として出力。 - OGP画像生成 — 記事ごとにOpen Graph画像をビルド時に生成。
- ページネーション — 1ページあたりの記事数を設定可能。
- 関連記事 — タグによる類似記事リンク。
- GitHubSourceリンク — Markdownソースへの編集リンクを自動追加。
- ライブリロード開発サーバー —
gohan serveがコンテンツを監視し保存時に自動再ビルド。
プラグインシステム
gohanはGoの標準pluginパッケージや外部ライブラリとしての設計ではなく、バイナリ内蔵型を選んだ。理由はシンプルで、「手軽かつシンプルに最短でSSGを使える体験」を優先したかったからだ。動的ロードやライブラリ依存を持ち込むと、インストール・ビルド・配布の手間が増える。問題が生じるまでは内蔵型で構わないという判断のもと、現在の設計に落ち着いている。
プラグインはgohanバイナリへコンパイルされ、config.yamlでプロジェクトごとに有効化する。利用者側の再コンパイルは不要だ。プラグインインターフェースはinternal/plugin/plugin.goに定義される。
type Plugin interface {
Name() string
Enabled(cfg map[string]interface{}) bool
TemplateData(article *model.ProcessedArticle, cfg map[string]interface{}) (map[string]interface{}, error)
}
type SitePlugin interface {
Name() string
Enabled(cfg map[string]interface{}) bool
VirtualPages(site *model.Site, cfg map[string]interface{}) ([]*model.VirtualPage, error)
}
Plugin(記事単位)は1つの記事に追加データを記事のテンプレートを通じて.PluginData.<name>として公開する。SitePlugin(サイト全体)は全記事処理後に実行され、Markdownソースを持たない仮想ページを生成できる。
内蔵レジストリには2つのプラグインが内蔵されている。
func DefaultRegistry() *Registry {
return &Registry{
plugins: []Plugin{
amazonbooks.New(),
},
sitePlugins: []SitePlugin{
bookshelf.New(),
},
}
}
amazon_booksは記事フロントマターのASIN値からAmazonアフィリエイト本カードデータ(画像・URL・タイトル)を生成する。bookshelfはサイト全体の本フロントマターを集約し履歴できる仮想/bookshelfページを生成する。
config.yamlでの設定例。
plugins:
amazon_books:
enabled: true
tag: "your-associate-tag-22"
bookshelf:
enabled: true
CLI リファレンス
gohan build
gohan build [--full] [--config=path] [--output=dir] [--parallel=N] [--dry-run]
| フラグ | 説明 |
|---|---|
--full |
前回マニフェストを無視してフルビルドを強制 |
--config |
設定ファイルのパス(デフォルト: ./config.yaml) |
--output |
出力ディレクトリの上書き |
--parallel |
並列ワーカー数(デフォルト: CPU数) |
--dry-run |
ファイルを書き出さずに変更対象を表示 |
--draft |
ドラフト記事(draft: true)もビルド対象に含める |
gohan new
gohan new [--title="タイトル"] [--type=post|page] <slug>
gohan serve
gohan serve [--port=N] [--host=addr]
| フラグ | 説明 |
|---|---|
--port |
ポート番号(デフォルト: 1313) |
--host |
ホストアドレス(デフォルト: 127.0.0.1) |
インストールと基本操作
# Homebrew (macOS/Linux)
brew install bmf-san/tap/gohan
# Go install
go install github.com/bmf-san/gohan/cmd/gohan@latest
# ビルド
gohan build
# ライブリロード付き開発サーバー
gohan serve
ユーザーガイド
詳細な設定やテンプレートの使い方はドキュメントを参照されたい。
| ガイド | 内容 |
|---|---|
| Getting Started | インストール、最初のサイト作成、ビルド、プレビュー |
| Configuration | config.yaml の全フィールドと Front Matter |
| Templates | テーマテンプレート・変数・組み込み関数 |
| Taxonomy | タグ・カテゴリー・アーカイブページ |
| CLI リファレンス | 全コマンドとフラグ |
まとめ
gohanはこのサイトを動かすエンジンだ。SHA-256マニフェストによるインクリメンタルビルドがイテレーションを速く保ち、コンパイル済みプラグインシステムがバイナリを自由に保つ。i18nからOGP、Mermaidまで、ビルド時はクライアントサイドJavaScript不要で動作する。
- GitHub: bmf-san/gohan