投稿のプレビューを表示する

中級 約10分 最終更新 2026-11-02

編集者は管理画面の投稿編集画面(SCR-104)で「プレビュー」ボタンを押すと、まだ公開していない下書きを、あなたの公開サイトと同じ見た目で確認できます。このとき開くページを公開サイト側でどう用意するか——種別ごとに登録するプレビューURLの仕組み、下書きを取得する cms.posts.getPreview()、そして静的サイト(SSG)でも成立する実行時レンダリングまでをまとめます。

前提:プレビュー取得APIは、通常の公開取得(get())が公開済みだけを返すのに対し、下書き版を返します。レスポンス形状は公開版と完全に同一(PostDetail)なので、公開ページの描画ロジックをそのまま再利用できます。現在、プレビューに対応するのは投稿(posts)のみです。

1. プレビュー機能とは

プレビューは「下書き(未公開)の内容を、公開したときと同じ表示で確認する」ための仕組みです。確認できるのは、未公開の新規投稿/公開済み投稿の未公開変更/変更のない公開済み投稿、のいずれも下書き版です。流れは次のとおりです。

  • ① 編集者がプレビューを押す:管理画面がその投稿のプレビュー用トークン(原本・プレフィックス pv_)を発行し、登録済みのプレビューURLに {SLUG}・{PREVIEW_TOKEN} を埋めたURLを新規タブで開きます。
  • ② あなたのサイトのプレビューページが開く:そのページがクエリの slug・token を読み、公開APIのプレビュー取得を呼びます。
  • ③ 下書きを描画する:返ってきた下書き版を、公開詳細ページと同じ描画で表示します。

トークンの有効期限は既定で24時間です。公開サイト側でやることは、プレビューURLの登録(2章)とプレビューページの用意(3〜4章)の2つだけです。

2. プレビューURLの仕組み

プレビューURLは投稿の種別ごとに、サイト設定画面(SCR-120・管理者ロール)でテンプレートとして登録します。管理画面はこのテンプレートの中のプレースホルダを置換して、実際に開くURLを組み立てます。

  • {SLUG} … 対象投稿の下書き版 slug に置換されます。
  • {PREVIEW_TOKEN} … 発行したプレビュー用トークン(原本)に置換されます。

テンプレートはあなたの公開サイト内のページを指すように設定します。例えば、お知らせ(種別 news)のプレビューページを /preview/posts/news/ に置くなら、次のように登録します(HTTPS必須。http://localhost・http://127.0.0.1 のみ例外)。

サイト設定(SCR-120)に登録するプレビューURLテンプレート
https://www.example.com/preview/posts/news/?slug={SLUG}&token={PREVIEW_TOKEN}
補足:種別ごとに未設定(null)にもできます。未設定の種別は、管理画面にプレビューボタンが表示されません。サイト設定取得(cms.siteSettings() 等)の post_types[].preview_url でも、各種別のテンプレートを確認できます。

3. getPreview() で下書きを取得する

下書きは cms.posts.getPreview(postType, slug, token) で取得します。内部的には GET /api/v1/posts/{postType}/{slug}/preview を呼び、トークンは X-PREVIEW-TOKEN ヘッダで送ります(ログへのトークン露出を避けるためクエリよりヘッダが推奨です。SDKは自動でヘッダに載せます)。既存の公開API認証(X-API-KEY+X-SITE-CODE)とは別レイヤの認証で、両方を満たす必要があります。

cURL で確認する
curl -i \
  -H "X-API-KEY: <APIキー>" \
  -H "X-SITE-CODE: <サイトコード>" \
  -H "X-PREVIEW-TOKEN: pv_8f32d7a8f9a34e2c1b0d5e6f7a8b9c0d" \
  "https://api.cms.sharedan.co.jp/api/v1/posts/news/new-office-opening/preview"

SDKなら下書き版の PostDetail が返ります。トークンが不正・期限切れ・失効(投稿/サイト削除・管理者による明示失効)・対象不一致(トークンのスコープと slug/サイトが不一致)の場合は、存在秘匿のためいずれも 404(ShareDanCmsApiError.isNotFound)に統一されます。401/403 で区別はしません。

src/lib/preview.ts
import { ShareDanCmsApiError, renderProseMirror } from '@sharedan/cms-sdk';
import { cms } from '../lib/cms';

// プレビューURL(.../preview/posts/news/?slug=...&token=...)の
// クエリから取り出した slug / token をそのまま渡す。
async function loadPreview(postType: string, slug: string, token: string) {
  try {
    // 下書き版(PostDetail)。形状は公開版 get() と完全に同一。
    const post = await cms.posts.getPreview(postType, slug, token);
    return post.body ? renderProseMirror(post.body) : '';
  } catch (err) {
    if (err instanceof ShareDanCmsApiError && err.isNotFound) {
      // トークンが不正/期限切れ(既定24h)/失効/対象不一致はすべて 404 に統一される。
      throw new Error('プレビューURLが無効か、有効期限が切れています');
    }
    throw err;
  }
}

4. プレビューページを作る(実行時取得)

プレビューURLのトークンは実行時にしか決まりません(編集者がボタンを押した瞬間に発行されます)。そのため、プレビューページの本文は SSGでビルド時に焼けません。ビルド時はページの「ガワ」だけを静的出力し、本文はブラウザから実行時に getPreview() を呼んで描画します。描画には公開詳細ページと同じレンダラ(renderProseMirror() など)を使い、下書きと公開版の見た目を揃えます。

src/pages/preview/posts/news/index.astro
---
// src/pages/preview/posts/news/index.astro
// CMSのプレビューURLテンプレートがこのページを指す:
//   https://<あなたのサイト>/preview/posts/news/?slug={SLUG}&token={PREVIEW_TOKEN}
// token は実行時にしか決まらないため、ここでは本文を焼かずガワだけを静的出力する。
---
<html lang="ja">
  <head>
    <!-- プレビューは検索エンジンにインデックスさせない(必須) -->
    <meta name="robots" content="noindex, nofollow" />
    <title>プレビュー</title>
  </head>
  <body>
    <div class="preview-banner">これは下書き(未公開)の内容です。</div>
    <article id="preview-root">プレビューを読み込んでいます…</article>

    <script>
      import { ShareDanCmsClient, ShareDanCmsApiError, renderProseMirror } from '@sharedan/cms-sdk';

      // 鍵はブラウザに出さない。同一オリジンのプロキシ(cms-proxy.php)が
      // X-API-KEY をサーバー側で付与し、X-PREVIEW-TOKEN は上流へ透過する。
      const cms = new ShareDanCmsClient({
        baseUrl: `${location.origin}/cms-proxy.php`,
        apiKey: 'via-proxy',
        siteCode: 'via-proxy',
      });

      const root = document.getElementById('preview-root')!;

      async function render() {
        const q = new URLSearchParams(location.search);
        const slug = q.get('slug');
        const token = q.get('token');
        if (!slug || !token) {
          root.textContent = 'プレビューURLが不正です。';
          return;
        }
        try {
          const post = await cms.posts.getPreview('news', slug, token);
          root.innerHTML = post.body ? renderProseMirror(post.body) : '';
          if (post.title) document.title = `${post.title}(プレビュー)`;
        } catch (err) {
          root.textContent =
            err instanceof ShareDanCmsApiError && err.isNotFound
              ? 'プレビューURLが無効か、有効期限が切れています。'
              : 'プレビューの取得に失敗しました。';
        }
      }
      render();
    </script>
  </body>
</html>
APIキーを露出させない:ブラウザから公開APIを直接叩くと読み取り用APIキーが露出します。パスワード保護された投稿や問い合わせフォーム連携と同じく、同一オリジンの薄いプロキシ(cms-proxy.php)を経由し、X-API-KEY の付与はサーバー側に任せます。プロキシは X-PREVIEW-TOKEN ヘッダを上流へ透過するだけで対応できます。

5. 注意点

  • キャッシュに載せない:プレビュー応答は成功・エラーとも Cache-Control: no-store です(下書きを変更した直後に最新を確認できるようにするため)。取得した下書きを自前のCDNやエッジにキャッシュしないでください。
  • 必ず noindex:プレビューページは <meta name="robots" content="noindex, nofollow"> を付け、検索エンジンにインデックスさせないでください。解析タグ(GTM等)も載せないのが無難です。
  • 返るのは下書き本文:公開版(get())とレスポンス形状は同一ですが、中身は未公開の下書きです。status などの内部状態フィールドは漏らさない設計のため含まれません。
  • トークンは短命・秘匿:トークンはURLに乗って編集者の手元に渡る性質上、URLを知る人は閲覧できます。有効期限(既定24時間)を過ぎる・管理者が失効させると 404 になります。ログやリファラにトークンが残らないよう、取得は X-PREVIEW-TOKEN ヘッダ(SDKの既定)で行いましょう。
  • 対象は投稿(posts)のみ:現在プレビュー取得APIが対応するのは投稿です。FAQ・固定コンテンツのプレビューは対象外です。
  • 多言語との関係:getPreview() は下書き(=編集中の原稿)を返すため、公開版の言語別取得(locale)とは別物で、locale は受け取りません(多言語サイトを作る)。
FEEDBACK

このページはお役に立ちましたか?

改善要望・不具合・ドキュメントの分かりにくい点など、お気軽にお寄せください。いただいた内容は今後の改善に役立てます。

返信をご希望の場合のみご記入ください。

スパム対策(レートリミット・ハニーポット・送信速度チェック)はサーバ側で自動適用されます。