はじめに
この記事は、エアークローゼットアドベントカレンダー2025の20日目の記事です。
「静的サイトでダイナミックルーティングなんて無理でしょ?」と言われたことはありませんか?
確かに、EleventyのようなStatic Site Generator(SSG)は静的なHTMLファイルを生成します。サーバーサイドレンダリングもなければ、ランタイム時のデータベースクエリもありません。
しかし、ちょっと待ってください——「静的」は「柔軟性がない」という意味ではありません。
今回は、私が「静的サイトでのダイナミックルーティング」と呼んでいるテクニックを紹介します。EleventyのPermalink機能とNunjucks、そしてJSON Data Filesを組み合わせることで実現できます。
結果は?たった1つのテンプレートから、何百、何千ものページを生成でき、データは完全に分離して管理できます。
EleventyとNunjucksとは?
Eleventy(11ty)
Eleventyは、JavaScriptで書かれたStatic Site Generator(SSG)です。シンプルで高速、そしてReactやVueなどのフロントエンドフレームワークを強制しないのが特徴です。
- ゼロコンフィグですぐに使える
- 複数のテンプレート言語に対応
- ビルドが非常に高速
- 出力は純粋なHTML、不要なJavaScriptを含まない
Nunjucks
Nunjucksは、Mozillaが開発したテンプレートエンジンで、PythonのJinja2にインスパイアされています。テンプレート継承、マクロ、フィルター、ループなどの強力な機能を備えています。
Eleventy + Nunjucksは人気の組み合わせです。Nunjucksの構文は読みやすく、静的ページ生成に便利な機能が豊富だからです。
🎯 実際のユースケース
50件のプロジェクトを持つポートフォリオサイトを構築するとします。各プロジェクトには個別のページが必要です:
/projects/sunrise-cafe/
/projects/tokyo-tower/
/projects/minimalist-apartment/
...
「従来の」方法(つらい)
src/
projects/
sunrise-cafe/index.njk
tokyo-tower/index.njk
minimalist-apartment/index.njk
... (あと47ファイル 😱)
問題点: レイアウトを変更したい?50ファイル全部修正。新しいフィールドを追加したい?50ファイル全部修正。悪夢です。
「スマートな」方法:Permalink + Data Files
src/
_data/
projects.json ← すべてのデータはここ
projects/
project.njk ← テンプレートは1つだけ
1つのデータファイル。1つのテンプレート。無限のページ。
🏗️ プロジェクト構成
eleventy-dynamic-routing/
├── src/
│ ├── _data/
│ │ └── projects.json
│ └── projects/
│ ├── index.njk (一覧ページ)
│ └── project.njk (各プロジェクトのテンプレート)
├── .eleventy.js
└── package.json
📦 Data File:システムの心臓部
src/_data/projects.jsonを作成します:
{
"items": [
{
"slug": "sunrise-cafe",
"title": "Sunrise Café",
"category": "インテリアデザイン",
"year": 2025,
"location": "東京"
},
{
"slug": "tokyo-tower",
"title": "東京タワー リニューアル",
"category": "建築",
"year": 2024,
"location": "東京"
},
{
"slug": "minimalist-apartment",
"title": "The White Canvas Apartment",
"category": "住宅",
"year": 2025,
"location": "大阪"
}
]
}
100件のプロジェクトを追加したい?items配列に100個のオブジェクトを追加するだけ。以上。
✨ Permalinkの魔法:1つのテンプレートで複数ページ
最も重要なファイル——src/projects/project.njk:
---
pagination:
data: projects.items
size: 1
alias: project
permalink: "projects/{{ project.slug }}/index.html"
---
<h1>{{ project.title }}</h1>
<p>カテゴリー: {{ project.category }}</p>
<p>所在地: {{ project.location }}</p>
<p>年: {{ project.year }}</p>
<a href="/projects/">← プロジェクト一覧に戻る</a>
これだけです!Eleventyが自動的に以下を生成します:
_site/projects/sunrise-cafe/index.html_site/projects/tokyo-tower/index.html_site/projects/minimalist-apartment/index.html
🔍 Front Matterの解説
---
pagination:
data: projects.items # _data/projects.jsonから読み込み
size: 1 # 1アイテム = 1ページ
alias: project # テンプレート内での変数名
permalink: "projects/{{ project.slug }}/index.html"
---
| プロパティ | 意味 |
|---|---|
data |
データソース、_data/projects.jsonから自動マッピング |
size: 1 |
配列内の各オブジェクト = 個別ページ |
alias |
テンプレートで使う変数名を定義 |
permalink |
これが「ルーター」! テンプレート文字列で動的URLを生成 |
📋 一覧ページ
すべてのプロジェクトを表示するsrc/projects/index.njkを作成:
---
title: プロジェクト一覧
---
<h1>プロジェクト一覧</h1>
<p>合計: {{ projects.items.length }}件</p>
<ul>
{% for project in projects.items %}
<li>
<a href="/projects/{{ project.slug }}/">
{{ project.title }}({{ project.year }})
</a>
</li>
{% endfor %}
</ul>
🚀 応用編:カテゴリー別ルート
src/_data/categories.jsonを追加:
[
{ "name": "インテリアデザイン", "slug": "interior-design" },
{ "name": "建築", "slug": "architecture" },
{ "name": "住宅", "slug": "residential" }
]
そしてsrc/categories/category.njk:
---
pagination:
data: categories
size: 1
alias: category
permalink: "category/{{ category.slug }}/index.html"
---
<h1>{{ category.name }}</h1>
<ul>
{% for project in projects.items %}
{% if project.category == category.name %}
<li>
<a href="/projects/{{ project.slug }}/">{{ project.title }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
これで以下のルートも追加されます:
/category/interior-design//category/architecture//category/residential/
🎨 関心の分離(Separation of Concerns)
┌─────────────────────────────────────────────────────┐
│ 従来の方法 │
├─────────────────────────────────────────────────────┤
│ project-1.md project-2.md project-3.md │
│ (データ+レイアウト) (データ+レイアウト) (データ+レイアウト) │
│ │
│ → レイアウト変更 = 全ファイル修正 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 新しい方法 │
├─────────────────────────────────────────────────────┤
│ projects.json project.njk │
│ (データのみ) → (レイアウトのみ) │
│ │
│ → レイアウト変更 = 1ファイルだけ │
│ → データ追加 = JSONに追加 │
│ → コンテンツ担当はJSON、開発者はテンプレートを編集 │
└─────────────────────────────────────────────────────┘
メリット
| やりたいこと | 従来の方法 | Data Filesを使う場合 |
|---|---|---|
| 100件追加 | 100ファイル作成 | JSONに追加 |
| レイアウト変更 | 100ファイル修正 | 1テンプレート修正 |
| CMSからインポート | 複雑 | JSONエクスポート |
⚠️ 注意:ルートの競合について
問題
もし物理的なファイル/フォルダがDynamic Routeと同じパスを持つ場合、ビルドエラーが発生します。
例えば、以下の状況:
src/
projects/
project.njk ← slug: "sunrise-cafe" を生成
sunrise-cafe/ ← 同じパスの物理フォルダが存在!
index.njk
この場合、両方とも /projects/sunrise-cafe/ を出力しようとして競合します。
解決方法
パターン1:物理ファイルを優先する場合
物理ファイル側で明示的にpermalinkを設定すれば、そちらが優先されます:
<!-- src/projects/sunrise-cafe/index.njk -->
---
permalink: "projects/sunrise-cafe/index.html"
---
<h1>カスタムページ(物理ファイル優先)</h1>
パターン2:JSONデータを優先する場合
物理ファイルの出力を無効にするには、permalink: falseを設定:
<!-- src/projects/sunrise-cafe/index.njk -->
---
permalink: false
---
これで物理ファイルは出力されず、JSONデータからのページが生成されます。
ベストプラクティス: 物理ファイルとJSONデータのslugが重複しないように命名規則を決めておくことをおすすめします。
🏁 まとめ
Eleventyは「静的」かもしれませんが、Pagination + Permalink + Data Filesを使えば、ダイナミックルーティングのような柔軟性を実現できます。
ポイント:
-
1テンプレートで無限ページ —
paginationでsize: 1を指定 - Permalinkが「ルーター」 — テンプレート文字列で動的URL生成
- JSONが「データベース」 — 編集・管理が簡単
- 関心の分離 — データとコードを完全に分離
エアークローゼット Advent Calendar 2025はまだまだ続きますので、ぜひ他のエンジニア, デザイナー, PMの記事もご覧いただければと思います
📚 参考資料
- Eleventy Documentation - Eleventy公式ドキュメント
- Eleventy Pagination - Paginationの詳細ガイド
- Eleventy Data Files - Global Data Filesの使い方
- Eleventy Permalinks - 出力URLのカスタマイズ
- Nunjucks Documentation - Nunjucks公式ドキュメント
- Nunjucks Templating Docs - Nunjucksの構文と機能