
静的サイトジェネレーター Zensical で「執筆者」と「サイト管理者」の関心を分離するテンプレートを作ってみた
こんにちは。クラウド事業本部コンサルティング部の桑野です。
皆さんは Zensical をご存知でしょうか?
Zensical はプロジェクトドキュメントの作成と管理を簡素化することができる静的サイトジェネレーターです。
Material for MKDocs の開発者によって開発され、MKDocs (およびその拡張機能の Material for MKDocs) との互換性を意識した作りとなっています。

弊社では、社内のエンジニアチームにてナレッジサイト(Classmethod Cloud Guidebook)を運営しています。
現在、ナレッジサイトは MkDocs (+ Material for MkDocs) を使って構成されています。
しかし、残念なことに2026年11月5日に Material for MkDocs が EOL を迎えます。
そうしたこともあって、後継である Zensical への移行を進めています。その一環として、今回のテンプレートを作成しました。
この記事で紹介するもの
Zensical を使って静的サイトを構築するにあたり、工夫した点をご紹介できればと思います。
具体的には、執筆者と管理者で触る場所を分けたテンプレートリポジトリを作成しました。
- 執筆者は
docs/配下の Markdown だけを触る - 管理者はサイト構築側(Zensical 設定・テーマ・アセット)を触る
- サイドバー(nav)は執筆者が編集する目次ページから自動生成する
作成物は以下のリポジトリにあります。本記事と併せて参照してください。
前提
以下の条件で検証しています。
- OS:macOS Tahoe バージョン 26.5.2
- チップ:Apple M4
- Colima:0.8.4
- Docker Client:28.4.0
- Docker Server:28.3.3
- docker compose:2.39.3
Macの場合、ColimaでDocker環境を簡単に作ることができます。セットアップ手順は以下の記事をご参照ください。
モチベーション
今回、筆者はナレッジサイトの Zensical への移行の検討を進めています。移行が完了したあとは、そのまま静的サイトの管理者としてサイトを運用していくことが想定されています。
想定している執筆者とサイト管理者の関係はこんなイメージです。
そこで、「自分が静的サイトの管理者になったときにどのようなことができると良いか」「現状のままだと何が困るか」を事前に考えてみました。その結果、以下の3点を課題として認識しました。
執筆者にサイト構築側のファイルを見せたくない
こちらは「リポジトリ上でどのファイルが見えるか」という観点の話です。
執筆者は「記事を書く」ことに集中したいので、Zensical の設定ファイル(zensical.toml)や CSS/JS、Dockerfile などのファイルは見えていない方が快適です。逆に管理者はサイト全体の構築設定に責任を持つため、こちらに集中したいです。ディレクトリの時点で役割ごとに触る場所を分けておくと、お互いに触る場所が明確になり、意図しない事故も起きにくくなります。
管理者側のメリットとしては、たとえば「UIのカスタマイズをしたい」「カスタム機能を導入したい」といった要望が出てきた際に、どこにどう手を入れるかという実装検討にそのまま集中できるようになります。サイト構築側のファイルが管理者領域にまとまっていれば、変更の影響範囲や責任範囲も自然と明確になります。
執筆者側のメリットとしては、普段裏側の仕組みが目に入らないため、意図せず(あるいは意識しない限り)サイト構築側の設定に変更を加えてしまう危険性を下げられます。管理者の目が届かないところで構成が書き換わっていた、という事故を構造的に防ぎたいという狙いです。
執筆者と管理者で開発体験を分けたい
こちらは「実際にどうやって開発するか」という観点の話です。前項の「見えるファイル」を分けたうえで、さらに開発時の操作手順や必要なツールも分けたいと考えました。
執筆者は VSCode でリポジトリを開いてタスクを実行するだけでプレビュー環境が立ち上がる、という体験が理想です。Dev Container のような開発環境の知識を要求されず、コンテナの中に入る必要もない状態を目指したいです。一方で管理者は Zensical のテーマや設定を触ることになるため、コンテナ内に入って開発できた方が便利です。同じリポジトリでも、両者に別々の開発体験を提供したいと考えました。
サイドバー(nav)の二重管理をなくしたい
Zensical(および元となる MkDocs)ではサイドバーの構成を zensical.toml の nav で定義します。一方、執筆者にとっては目次ページ(sitemap.md 相当)が見えていた方が全体像を把握しやすいです。両方をそれぞれ手書きするとズレていくのが目に見えていたため、片方を編集すればもう片方が自動で追従する仕組みにしたいと考えました。
リポジトリ全体の構成
Zensical はプロジェクト構成に一定の前提を持っています。特に大きいのが、サイトに配置するファイル(Markdown、CSS、JS、画像)はすべて docs/ 配下に置く必要があるという制約です。追加の CSS/JS も docs_dir(= docs/)からの相対パスで指定します。
extra_css = [
"stylesheets/extra.css",
]
extra_javascript = [
"javascripts/tablesort.js",
]
この仕様に素直に従うと、CSS/JS も docs/stylesheets/, docs/javascripts/ に置くことになり、執筆者領域である docs/ にサイト構築側のファイルが混ざってしまいます。Zensicalの前提のままでは、執筆者と管理者の触る場所を分けるのが難しい、というのが出発点です。
なお、docs_dir や site_dir をプロジェクトフォルダの外に置けるようにしてほしい、というリクエストは Zensical 側にも上がっており、以下の Issue で議論されています。現状は .. を含むパスや絶対パスは許可されないため、docs/ の外に執筆者コンテンツを置くことはできません。
そこで今回のテンプレートでは、Docker のボリュームマウントを使ってこのギャップを吸収しています。ホスト側のディレクトリ構成と、コンテナ内のディレクトリ構成の2つを、それぞれ以下のように構成しています。
ホスト側のディレクトリ構成
zensical-test/
├── .vscode/ # 執筆者向けVSCodeタスク
├── docker-compose.yaml # 開発コンテナ定義
├── docs/ # 執筆コンテンツ(執筆者領域)
│ ├── README.md
│ ├── sitemap.md # サイドバーの生成元
│ ├── getting-started/
│ └── guides/
└── ssg/ # サイト構築設定(管理者領域)
├── .devcontainer/ # 管理者向けDev Container定義
├── .vscode/ # 管理者向けVSCodeタスク
├── CLAUDE.md
├── Dockerfile
├── README.md
├── assets/ # CSS/JS
│ ├── stylesheets/
│ └── javascripts/
├── notes/ # 管理者向け設計判断・運用メモ
├── overrides/ # Zensicalテンプレートオーバーライド
├── pyproject.toml
├── scripts/ # nav自動生成スクリプト、zensicalラッパー
├── site/ # Zensicalビルド成果物
└── zensical.toml # Zensical設定ファイル
docs/ は執筆者領域、ssg/ は管理者領域として明確に分かれています。CSS/JS も ssg/assets/ 配下に置かれており、執筆者領域には現れません。
コンテナ内のディレクトリ構成
/ssg/ # Zensicalのワークスペースルート
├── zensical.toml
├── pyproject.toml
├── overrides/
├── scripts/
├── site/
└── docs/ # ← ホスト側の docs/ をマウント
├── README.md
├── sitemap.md
├── getting-started/
├── guides/
├── stylesheets/ # ← ホスト側の ssg/assets/stylesheets/ をマウント
└── javascripts/ # ← ホスト側の ssg/assets/javascripts/ をマウント
コンテナ内では、Zensical の期待どおり /ssg/docs/ 配下に CSS/JS を含む全ファイルが揃った状態になります。zensical.toml の extra_css / extra_javascript の指定もそのまま機能します。
ホスト側で分けてあるファイルを、コンテナ内では Zensical が扱える形にまとめ直す。これを実現しているのが Docker のボリュームマウントです。次節で具体的な設定を見ていきます。
工夫1: Dockerボリュームマウントでコンテナ内外の見え方を切り替える
前節のホスト側とコンテナ内のギャップは、docker-compose.yaml のマウント設計で吸収しています。
services:
ssg:
build:
context: ./ssg
dockerfile: Dockerfile
volumes:
# Zensicalプロジェクトの構成ファイル
- ./ssg/zensical.toml:/ssg/zensical.toml
- ./ssg/pyproject.toml:/ssg/pyproject.toml
- ./ssg/uv.lock:/ssg/uv.lock
- ./ssg/.python-version:/ssg/.python-version
- ./ssg/overrides:/ssg/overrides
# 開発環境の設定
- ./ssg/.vscode:/ssg/.vscode
- ./ssg/.devcontainer:/ssg/.devcontainer
- ./ssg/Dockerfile:/ssg/Dockerfile
- ./ssg/CLAUDE.md:/ssg/CLAUDE.md
- ./ssg/notes:/ssg/notes
- ./ssg/scripts:/ssg/scripts
- ./ssg/site:/ssg/site
- ./ssg/README.md:/ssg/README.md
# 執筆コンテンツ
- ./docs:/ssg/docs
# ssg/assets/ 配下を docs 配下に重ね着
- ./ssg/assets/stylesheets:/ssg/docs/stylesheets
- ./ssg/assets/javascripts:/ssg/docs/javascripts
ポイントは大きく2つあります。
1つ目は、ホスト側の ssg/ 配下と docs/ を、コンテナ内の /ssg/ 配下にまとめてマウントしていることです。ホスト側では別ディレクトリに分かれていた両者が、コンテナ内では Zensical が期待する /ssg 直下に統合された状態になります。
2つ目は、CSS/JS を配置している ssg/assets/stylesheets/, ssg/assets/javascripts/ を、コンテナ内では /ssg/docs/stylesheets/, /ssg/docs/javascripts/ として「重ね着」させていることです。ホスト側では執筆者領域 docs/ に現れないファイルが、コンテナ内では Zensical から見て docs/ 配下に置かれているように見えます。
ただし、このオーバーレイマウントの副作用として、ホスト側の docs/stylesheets/, docs/javascripts/ がマウントポイントとして空ディレクトリで残ってしまいます。執筆者から見ると「触ってよいのか分からないディレクトリ」に見えてしまうため、リポジトリルートの .gitignore で以下のように除外しています。
/docs/stylesheets/
/docs/javascripts/
これで、執筆者がリポジトリをクローンしたときにも副作用ディレクトリは追跡対象にならず、「ここは触るものではない」という認識を持ってもらいやすくなります。
この2つのマウント設計により、以下が両立できます。
- ホスト側: 執筆者領域(
docs/)と管理者領域(ssg/)が明確に分かれ、CSS/JS も執筆者から見えない - コンテナ内(Zensical から見たディレクトリ構造): すべてのファイルが
/ssg/docs/配下に揃い、Zensical の制約を満たす
執筆者は docs/ を開いても Markdown コンテンツしか目に入らず、zensical serve や zensical build の裏側でコンテナがファイル配置のギャップを吸収してくれる、という体験になります。
工夫2: サイドバー(nav)をsitemap.mdから自動生成する
Zensical のサイドバーは zensical.toml の nav で定義しますが、この形式は執筆者が編集するには少し窮屈です。加えて、zensical.toml は静的サイトの設定そのものであり、管理者としても執筆者に不用意に触らせたくない領域です。
nav = [
"README.md",
"sitemap.md",
{ "はじめに" = [
"getting-started/README.md",
"getting-started/installation.md",
"getting-started/configuration.md",
] },
]
一方、執筆者にとっては目次ページの形式の方が全体像が把握しやすいです。
- [ホーム](README.md)
- [サイトマップ](sitemap.md)
## はじめに
- [概要](getting-started/README.md)
- [インストール](getting-started/installation.md)
- [設定](getting-started/configuration.md)
両方を手書きすると必ずズレが発生するため、目次ページ(docs/sitemap.md)だけを編集すれば、zensical.toml の nav が自動で追従する仕組みを作りました。
生成スクリプト(gen_nav.py)
ssg/scripts/gen_nav.py が sitemap.md をパースして nav を生成します。抽出ルールは以下のとおりです。
- H1
# ...: ページタイトル。無視する - H2 以降
## .../### ...: セクション。見出しレベルでネストする - [ラベル](path): ページ。直近セクション配下に出現順で追加する- 見出しより前のトップレベル箇条書き : ルート直下ページ
zensical.toml は以下のように # ### SITEMAP BEGIN ### 〜 # ### SITEMAP END ### のマーカーで囲まれた範囲だけを置換対象にしています。他の設定行には一切触れません。
# ### SITEMAP BEGIN ###
# nav は docs/sitemap.md から自動生成される。手で編集しない。
nav = [
...
]
# ### SITEMAP END ###
また、docs/ 配下に存在するのに sitemap.md に未掲載の Markdown ファイルは警告として出力します。これは「サイドバーに載せ忘れた」ケースの取りこぼし検出になります。
zensicalコマンドのシェルラッパーで自動実行する
nav 生成スクリプトを手動で実行するのは忘れそうなため、zensical serve / zensical build の実行時に自動で走るようにしました。
ssg/scripts/zensical にシェルスクリプトを置き、Dockerfile で /usr/local/bin/zensical に配置しています。
COPY scripts/zensical /usr/local/bin/zensical
RUN chmod +x /usr/local/bin/zensical
/usr/local/bin は PATH 上で .venv/bin より前方にあるため、zensical コマンドはまずこのラッパーに解決されます。ラッパーの中では、serve / build サブコマンドの場合のみ gen_nav.py --write を実行し、その後に本物の zensical に処理を委譲します。
case "${1:-}" in
serve | build)
if [ -f "${GEN_NAV}" ]; then
echo "zensical(wrapper): sitemap.md から nav を再生成します..." >&2
uv run --project "${PROJECT_ROOT}" python "${GEN_NAV}" --write >&2 \
|| echo "zensical(wrapper): nav 再生成に失敗しました" >&2
fi
;;
esac
run_real_zensical "$@"
これにより、執筆者・管理者・CI のいずれの経路から zensical を叩いても、サイドバーが常に sitemap.md と同期される状態を保てます。呼び出し側は従来どおり zensical serve と打つだけでよく、nav 生成の存在を意識する必要はありません。
執筆者と管理者、それぞれの開発体験
前述の工夫を踏まえて、執筆者と管理者は以下のような開発体験になります。
執筆者の場合
ホスト側の VSCode でリポジトリを開き、Cmd+Shift+P → Tasks: Run Task からタスクを実行します。
- コンテナビルド(初回のみ)
- コンテナ起動
- プレビュー起動 → ブラウザで
http://localhost:8001を開く docs/の Markdown を編集するとホットリロード
執筆者は docs/ 配下の Markdown だけを触ります。CSS/JS もサイト設定もサイドバー定義も目に入りません。
管理者の場合
Dev Container 拡張機能を使って、コンテナ内に接続します。
- コンテナ起動 タスクを実行
Cmd+Shift+P→Dev Containers: Reopen in Containerでssgサービスに接続/ssgがワークスペースルートとして開かれる
Dev Container の設定は ssg/.devcontainer/devcontainer.json に配置しています。管理者はコンテナ内の /ssg で作業するため、Zensical の設定ファイルやテーマ、アセットなどサイト構築に必要な全ての要素にアクセスできます。
最後に
Zensical で静的サイトを構築するにあたって、執筆者と管理者で触る場所を分けたテンプレートを作成した話をご紹介しました。
まだ実際にチームで運用できているわけではなく、「ひとまずこういう形で試してみるのはどうか」という段階です。自分で執筆者役と管理者役の両方を触ってみた限りでは、やりたい作業に対してディレクトリ構成レベルでの分離はできていそうだと感じています。ただし、実際に執筆者に使ってもらってみないと見えてこない部分も多いと思うので、フィードバックを受けて適宜改善していく前提であり、これが完成形というわけではありません。
また、本記事の中でも触れたように、Zensical はまだロードマップや Issue の対応が追いついていない部分もあり、docs_dir の扱いや nav 定義といった今回工夫で対応した点についても、公式機能としてサポートされるようになると嬉しいなと感じています。とはいえ、Zensical の Issue を眺めていると多くの要望が上がっており、まだ発展途上のプロダクトとして今後の進化にも大いに期待しているところです。
「執筆者と管理者の関心を分けたい」という課題に対する一つのアプローチとしてご紹介しました。同じような悩みを持つ方の参考になれば嬉しいです。冒頭でご紹介したナレッジサイト(Classmethod Cloud Guidebook)の Zensical 移行の中でも、本テンプレートの考え方をベースに構成を組み立てていく予定です。
最後までご覧いただきありがとうございました。





