Astro + Keystatic で、管理画面を本番に出さないブログを作る
このサイトは、Astro で作った静的サイトです。記事は Keystatic という管理画面で書いています。
Keystatic は便利なのですが、そのまま使うと、管理画面を動かすためのサーバー処理が必要になります。せっかくの静的サイトなので、本番には管理画面を一切置かず、HTML だけを配信したいと考えました。
この記事では、そのために選んだ構成と、途中でつまずいたところをまとめます。
作りたかったもの
- 記事は、ブラウザの管理画面で書きたい(Markdown を手で書くより楽なので)
- 本番のサーバーには、管理画面もログイン機能も置かない
- 記事のデータは Git で管理して、複数の PC から書けるようにする
構成
Keystatic には、記事を GitHub に直接保存するモードと、手元のファイルに保存する「ローカルモード」があります。今回はローカルモードを選びました。
流れは次のようになります。
- 手元の PC で開発サーバー(npm run dev)を起動する
- ブラウザで管理画面(/keystatic)を開いて、記事を書く
- 記事は Markdown(正確には Markdoc)のファイルとして保存されるので、Git でコミットして push する
- サーバー側でビルドして、できた HTML を公開する
管理画面は手元の PC の中でしか動かないので、インターネットからは管理画面にたどり着けません。ログインの仕組みを作る必要もありません。
管理画面を開発時だけ有効にする
ポイントは、Keystatic の設定を、開発サーバーのときだけ読み込むようにすることです。
環境変数で切り替える方法もありますが、サーバーで設定を忘れると、本番に管理画面が混ざってしまいます。そこで、Astro が「今は開発サーバーなのか、本番のビルドなのか」を教えてくれる情報を使って判定するようにしました。これなら、サーバー側で何も設定しなくても、本番のビルドには管理画面が入りません。
管理画面は React で動いているので、React の読み込みも同じように、開発時だけにしています。
具体的には、インテグレーション(Astro の拡張機能)を包んで、開発サーバーのときだけ動かす関数を作りました。
function devOnly(integration) {
let enabled = false;
const hooks = {};
for (const [name, hook] of Object.entries(integration.hooks)) {
if (typeof hook !== 'function') continue;
hooks[name] = (options) => {
// astro:config:setup は、ほかのフックより先に呼ばれる
if (name === 'astro:config:setup') enabled = options.command === 'dev';
if (enabled) return hook(options);
};
}
return { name: `${integration.name} (dev only)`, hooks };
}
Astro は起動するときに、最初に astro:config:setup というフックを呼び出し、その中で「開発サーバー(dev)なのか、ビルド(build)なのか」を教えてくれます。そこで開発サーバーのときだけ有効にして、それ以外のときは、元のインテグレーションの処理を何も実行しないようにしています。
あとは、Keystatic と React をこの関数で包んで登録するだけです。
integrations: [devOnly(react()), devOnly(keystatic())],
ただし、この方法は「React を管理画面でしか使っていない」ことが前提です。サイトのページで React を使う場合は、React は包まずに登録する必要があります。
つまずいたところ:下書きの画像が本番に漏れていた
記事には「下書き」のフラグを付けられるようにして、下書きの記事は本番に出さないようにしました。
最初は、記事の一覧や記事のページを作るときに、下書きを除外していました。これで、下書きの記事のページは本番に出なくなります。
ところが、ビルドの結果を調べてみると、下書きの記事に設定したアイキャッチ画像だけが、本番の画像フォルダにコピーされていました。ページは出ていなくても、画像のファイルとしては公開されてしまう状態です。
原因は、Astro が記事のデータを読み込んだ時点で、画像の処理も始めてしまうことでした。ページを作る段階で記事を除外しても、もう遅かったのです。
そこで、本番のビルドでは、記事のデータを読み込む段階で、下書きを読み込まないようにしました。これで、下書きの記事に関するものは、画像も含めて本番に一切出なくなりました。
「ページに出ていない = 公開されていない」とは限らない、というのが今回の一番の学びです。非公開にしたいものは、できるだけ手前の段階で除外するのが安全だと思いました。
本番への反映
サーバー側では、最新の記事を取得してビルドし、できた HTML を公開フォルダに反映しています。反映には rsync の --delete を使っていて、公開フォルダをビルドの結果とまったく同じ状態にします。
ただ、--delete は便利な反面、ビルドに失敗して中身が空だったりすると、公開中のサイトを丸ごと消してしまいます。そこで、反映の前に次のチェックを入れて、どれかに当てはまったら反映せずに止めるようにしました。
- ビルドが失敗していないか
- トップページ(index.html)がちゃんとできているか
- ビルドの結果に、管理画面(keystatic)に関するファイルが混ざっていないか
if [ ! -f dist/index.html ]; then echo "dist/index.html がありません。反映を中止します" exit 1 fi if find dist -iname '*keystatic*' | grep -q .; then echo "dist に keystatic が含まれています。反映を中止します" exit 1 fi
この一連の処理は、自作の管理パネルのボタン1つで実行できるようにしています。
複数の PC で書くときの工夫
記事のデータは Git で管理しているので、PC を変えても、clone して開発サーバーを起動すれば、すぐに続きを書けます。
Windows と Linux を行き来するので、改行コードがずれないように、.gitattributes で LF に統一しました。
* text=auto eol=lf
あとは、「作業の前に git pull、終わったら git push」を守るだけです。
細かいところ
- 記事の URL(スラッグ)は、日本語のタイトルから自動では作れないので、半角英数字で手入力しています。
- 開発サーバーを起動したままビルドすると、開発サーバーの表示が一時的におかしくなることがあります。再起動すれば直ります。
まとめ
- Keystatic のローカルモードを使い、管理画面は手元の PC でだけ動かす
- 本番のビルドには、管理画面を含めない(開発時かどうかは、Astro の情報で判定する)
- 下書きは、ページを作る段階ではなく、データを読み込む段階で除外する
- 反映の前に、空のビルドや管理画面の混入をチェックする
管理画面の書きやすさと、静的サイトの安全さ・軽さを両立できて、満足しています。