Astroの静的ブログに下書きプレビューを追加するには、公開ページを静的生成のまま残し、プレビュー専用のルートをリクエスト時にレンダリングします。genkoでは、その記事のdraftKeyとサーバー側のAPIキーを使い、SDKのgetOrNull(id, { draftKey })で保存済みの原稿を取得できます。アダプターを追加すれば、公開記事の配信方法を変えずに編集者向けの確認画面を用意できます。

本記事はgenko.meの運営側が執筆しています。2026年10月21日に下書きプレビューのドキュメント、Astro実践ガイド、SDK・管理画面の実装を確認しています。

公開ページとプレビューの役割を分けます

公開用の/blog/[id]はビルド時に公開記事を取得し、確認用の/preview/[id]はアクセス時に保存済み下書きを取得します。プレビュー用のIDをgetStaticPaths()へ追加したり、下書き入りのHTMLを公開用ビルドへ混ぜたりする必要はありません。

ルート

実行タイミング

取得するもの

/blog

ビルド時

公開記事の一覧

/blog/[id]

ビルド時

公開記事の本文

/preview/[id]?draftKey=...

リクエスト時

キーに対応する保存済み原稿

Astroのオンデマンドレンダリングでは、アダプターを設定し、必要なページへexport const prerender = falseを指定できます。ここではその仕組みを専用ルートだけに使います。静的ファイルだけを配るホスティングでは、このサーバー処理を実行できません。

公開ページとプレビューで記事のレイアウトを共通化すると、文字組み・画像サイズ・余白が揃います。取得処理とプレビューの注意書きは、その共通部品の外に置きます。公開ページ全体の作成はAstroでブログを作る手順、基本の接続はAstro導入ガイドを参照してください。

genkoのプレビュートークンはdraftKeyです

ここで扱うトークンの名前はdraftKeyです。任意のプレビュー用シークレットをサイトで発行して下書き一覧を読む仕組みではありません。対象記事のキーと通常の配信APIキーを組み合わせ、リスト形式の単一記事、またはオブジェクト形式のコンテンツを取得します。

SDKでは次のメソッドを使えます。clientはcreateGenko(...).apisです。

ts
// リスト形式の単一記事const post = await client.blog.getOrNull(id, { draftKey });
// オブジェクト形式。こちらは記事IDを渡しません。const page = await client.profile.getOrNull({ draftKey });

getOrNullは404のときにnullを返します。401・403・通信エラーまで「記事なし」にまとめるわけではありません。通常のgetを使う場合は404も例外になるため、画面側のエラー処理を変えます。

確認したSDKでは、draftKeyをAPIのURLではなくX-Draft-Keyヘッダーへ載せ、APIキーをX-API-KEYヘッダーへ載せます。編集者が開くプレビューURLにキーがあることと、サーバーから配信APIへ送る方法は分けて考えてください。配信API仕様でも、直接取得する場合はX-Draft-Keyが推奨されています。

プレビュー対象は保存済み原稿です。公開中の記事に未公開の変更を保存した場合は、その変更を確認できます。まだエディタで入力しただけの内容は取得できません。また、親記事をプレビューしても、参照先の下書きまで一緒に取得できるわけではありません。

Nodeアダプターを追加します

既存のAstroプロジェクトで、SDKとNode向けアダプターを追加します。

bash
npm install @genko-me/sdk @astrojs/node
js
// astro.config.mjsimport { defineConfig } from "astro/config";import node from "@astrojs/node";
export default defineConfig({  output: "static",  adapter: node({ mode: "standalone" }),});

既存の設定にintegrationなどがある場合は残し、必要な設定を追加してください。この例はNodeの実行環境を前提にします。別のアダプターを使う場合は、秘密値の読み方とサーバーの起動方法をデプロイ先に合わせます。

APIキーはホスティング先のサーバー環境へGENKO_API_KEYとして登録します。記事中の値をそのまま使うのではなく、対象のblogAPIを読めるキーを発行してください。PUBLIC_GENKO_API_KEYのような公開用変数や、ブラウザへ渡すpropsには入れません。Astroの環境変数ガイドも確認してください。

スキーマとプレビュー専用クライアントを用意します

CMS側にリスト形式のblogAPIを作り、titleをテキスト、bodyをリッチテキストとして登録します。次の型宣言だけでCMSのフィールドが作られるわけではありません。

ts
// src/lib/schema.tsimport { listApi } from "@genko-me/sdk";
export type Blog = { title: string; body: string };
export const apis = {  blog: listApi<Blog>({    fields: [      { id: "title", type: "text" },      { id: "body", type: "richtext" },    ],  }),};
ts
// src/lib/genko-preview.tsimport { createGenko } from "@genko-me/sdk";import { apis } from "./schema";
export function createPreviewClient() {  const apiKey = process.env.GENKO_API_KEY;  if (!apiKey) throw new Error("GENKO_API_KEY is required");
  return createGenko({    workspace: "your-workspace",    apiKey,    apis,    fetch: (input, init) =>      fetch(input, { ...init, cache: "no-store" }),  }).apis;}

ワークスペースIDを置き換え、サーバーでだけこの関数を呼びます。Nodeのprocess.envから実行時にキーを読み、プレビュー用の取得をno-storeにしています。既存の公開ページ用クライアントにキャッシュを設定していても、この関数を分けておけば、その設定を下書きへ持ち込みにくくなります。

SSRのプレビュールートを作ります

次のページにはgetStaticPaths()を書きません。IDとキーはアクセス時に受け取り、キーが空・重複・長すぎる場合はAPIへ問い合わせる前に拒否します。

astro
---// src/pages/preview/[id].astroexport const prerender = false;
import { createPreviewClient } from "../../lib/genko-preview";import type { Blog } from "../../lib/schema";
Astro.response.headers.set("Cache-Control", "private, no-store");Astro.response.headers.set("X-Robots-Tag", "noindex, nofollow");Astro.response.headers.set("Referrer-Policy", "no-referrer");
const id = Astro.params.id;const keys = Astro.url.searchParams.getAll("draftKey");let post: (Blog & { id: string }) | null = null;let message = "プレビューを表示できません。";
if (!id || keys.length !== 1 || !keys[0] || keys[0].length > 64) {  Astro.response.status = 400;  message = "管理画面からプレビューを開き直してください。";} else {  try {    const client = createPreviewClient();    post = await client.blog.getOrNull(id, { draftKey: keys[0] });    if (!post) {      Astro.response.status = 404;      message = "原稿またはプレビューキーを確認してください。";    }  } catch {    Astro.response.status = 502;    message = "取得に失敗しました。サイト管理者へご連絡ください。";  }}---<html lang="ja">  <head>    <meta charset="utf-8" />    <meta name="robots" content="noindex, nofollow" />    <meta name="referrer" content="no-referrer" />    <title>下書きプレビュー</title>  </head>  <body>    <aside>保存済み原稿のプレビューです。</aside>    <main>      {post ? (        <article>          <h1>{post.title}</h1>          <div set:html={post.body} />        </article>      ) : <p>{message}</p>}    </main>    <a href="/blog">公開記事一覧へ戻る</a>  </body></html>

キーが無効なときに、キーなしで取り直して公開版を表示する処理は入れません。公開中の記事では、誤ったキーでも画面が出るとプレビューできているように見えるためです。空文字をundefinedへ変えて通常取得に切り替える実装にも注意してください。

この例は、記事が存在しない場合と取得に失敗した場合を分けています。本番では、設定ミス、APIの認証失敗、ネットワーク障害を内部の監視で切り分けてください。生の例外、リクエストURL、認証情報、下書き本文を画面やログに出さないでください。

set:htmlはgenkoのリッチテキストbodyを表示する前提です。別の入力元のHTMLを無条件で差し込む用途には流用しないでください。レイアウトを共通化するときは、このarticle部分を既存の記事表示コンポーネントへ置き換えます。

管理画面のプレビューURLを合わせます

対象APIの「設定」で、プレビューURLを次の形に設定します。

text
https://your-site.example/preview/{id}?draftKey={key}

現在の管理画面では、末尾のdraftKey={key}は固定の補助表示として扱われます。編集欄がURL本体だけを受け付ける場合はhttps://your-site.example/preview/{id}を入力し、完成後の表示でキーが一度だけ付くことを確認してください。テンプレート全体を入力する形式なら上の完成形を使います。どちらの形式で入力しても、キーが一度だけ付くよう正規化されます。既存クエリがある場合は&draftKey={key}になります。

管理画面は{id}を記事ID、{key}を対象原稿のプレビューキーへ置き換えます。エディタの「プレビュー」操作ではキーを確保し、組み立てたURLを別タブで開く実装です。編集者がAPIキーをコピーしたり、開発者へ毎回URLの発行を頼んだりする必要はありません。

編集者の操作は、記事を保存し、「プレビュー」で表示を確認し、必要なら原稿を直してもう一度保存する流れです。プレビューページを更新すれば保存済みの変更を取得します。公開中の記事への未公開変更は「下書きとして保存」で確認でき、通常の公開ページには公開済みの内容が残ります。

HTMLとURLの両方を守ります

APIキーをサーバーだけに置いても、プレビューURLに含まれるdraftKeyは慎重に扱います。URLを知る人がプレビュー画面へ到達できる構成なので、社内チャットの公開チャンネルや問い合わせ本文へ貼らないようにしてください。

上の例では取得時とHTML応答の両方をキャッシュしない設定にし、検索除外とリファラー抑制も設定しています。さらにCDNやリバースプロキシ側で/preview/*を共有キャッシュの対象から外します。アクセスログや解析へクエリ文字列を残さない設定も確認してください。noindexは認証の代わりにはなりません。

プレビューには解析、広告、共有ボタンを入れないでください。サイトにログイン中のスタッフだけに限定したい場合は、サイト側の認証かアクセスゲートウェイを追加します。公開後は記事の「設定」でプレビューキーを再生成すると、以前のURLは無効になります。

専用ルートなので、終了操作は公開一覧へ戻す形にしています。URLからdraftKeyだけを削除しても/preview/[id]のままで、この実装では400になります。公開詳細へ戻したい場合は、公開済みであることを確認したうえで/blog/[id]へのリンクを別に用意します。

多言語と公開反映は別途扱います

多言語のプレビューでは、テンプレートに{lang}も追加できます。

text
https://your-site.example/preview/{id}?lang={lang}&draftKey={key}

この場合はSSR側でlangを受け取り、対応言語を検証したうえでgetOrNull(id, { draftKey, lang })へ渡します。言語版ごとにキーが異なるため、上の単一言語のサンプルへURLだけを追加して終わりにしないでください。多言語コンテンツに言語別の配信条件があります。

プレビューが最新でも、公開用の静的ページは再ビルドまで更新されません。保存済み下書きを見る経路と、公開後にHTMLを作り直す経路を分けて確認してください。公開反映はAstro実践ガイドのWebhook連携も参考になります。

導入時に確認するケース

最初は未公開の記事を保存し、管理画面から開いて本文が表示されることを確認します。次に空のキー、不正なキー、キーの重複、キー再生成後の古いURLを試し、公開版へ切り替わらないことを確認します。

公開中の記事を下書きとして保存するケースでは、プレビューだけに変更が出るかを確認します。最後にアダプターを含む本番相当の起動方法でSSRルートが動き、CDNがプレビューHTMLを保持していないことを確認してください。開発サーバーで表示できることと、静的ファイルだけの配信先で動くことは別です。

よくある質問

Astroの静的ホスティングだけで下書きを確認できますか?

この記事の方法にはプレビュールートを実行するサーバーが必要です。公開ページを静的に保ちつつ、アダプターを使った実行環境を用意します。

genkoのAPIキーだけで下書きを取得できますか?

対象記事のdraftKeyも必要です。一覧APIへキーを付けても、すべての下書きを返す一覧にはなりません。

保存していない編集内容もプレビューに反映されますか?

プレビューの対象は保存済みの原稿です。エディタで保存してからプレビューを開くか、開いているプレビューを更新してください。

Next.jsのDraft ModeをAstroでも使いますか?

この実装では使いません。AstroのSSRルートでURLを受け取り、SDKへdraftKeyを渡すことで取得します。

プレビューできれば公開ページも更新済みですか?

プレビューはリクエスト時に取得しますが、静的な公開ページは別途再ビルドが必要です。公開後のWebhookとビルド結果も確認してください。