技術

Astro + Keystatic で、管理画面を本番に出さないブログを作る

書いた人:

このサイトは、Astro で作った静的サイトです。記事は Keystatic という管理画面で書いています。

Keystatic は便利なのですが、そのまま使うと、管理画面を動かすためのサーバー処理が必要になります。せっかくの静的サイトなので、本番には管理画面を一切置かず、HTML だけを配信したいと考えました。

この記事では、そのために選んだ構成と、途中でつまずいたところをまとめます。

作りたかったもの

  • 記事は、ブラウザの管理画面で書きたい(Markdown を手で書くより楽なので)
  • 本番のサーバーには、管理画面もログイン機能も置かない
  • 記事のデータは Git で管理して、複数の PC から書けるようにする

構成

Keystatic には、記事を GitHub に直接保存するモードと、手元のファイルに保存する「ローカルモード」があります。今回はローカルモードを選びました。

流れは次のようになります。

  1. 手元の PC で開発サーバー(npm run dev)を起動する
  2. ブラウザで管理画面(/keystatic)を開いて、記事を書く
  3. 記事は Markdown(正確には Markdoc)のファイルとして保存されるので、Git でコミットして push する
  4. サーバー側でビルドして、できた 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 の情報で判定する)
  • 下書きは、ページを作る段階ではなく、データを読み込む段階で除外する
  • 反映の前に、空のビルドや管理画面の混入をチェックする

管理画面の書きやすさと、静的サイトの安全さ・軽さを両立できて、満足しています。

← 記事一覧へ