オブジェクトのプレビュー機能を実装

オブジェクト(例えば記事)のプレビューを実装してみました。実装方法は「Advanced Features: Preview Mode | Next.js」で解説されていますが、当サイトの場合はpage/preview.jsの新規実装とpage/blog/[basename].jpの編集、そしてAmplifyへの環境変数の設定を行いました。

PowerCMS Xのプレビューボタンを押すと記事のベースネームとシークレットトークンを付けてプレビューのリクエストを送信します。プレビューボタンは「管理画面のカスタマイズ | PowerCMS X」で紹介されているように代替テンプレートを用意したのですが、今後プラグイン化しようと考えています。(※2021年11月5日追記:プラグイン化が完了しています。)
PowerCMS Xのオブジェクト編集画面をカスタマイズしてプレビューボタンを設置した様子

シークレットトークンについてはPHPでecho bin2hex( random_bytes( 16 ) );を使い生成してみました。下書きの記事をAPIで取得するには必要な権限を有するユーザーで認証を行う必要があります。結果、page/preview.jsは以下のようなコードになりました。

import { client } from '../../libs/client';

export default async function handler(req, res) {
  if (req.query.endpreview) {
    // /api/preview?endpreview=1でアクセスするとプレビューデータをクリアする
    res.clearPreviewData();
    res.json({ message: 'Successfully cleared the preview mode cookie.' })
    res.end();
    return;
  }

  if (req.query.secret !== process.env.CMS_PREVIEW_TOKEN || !req.query.basename) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  // ユーザー認証
  const name = process.env.CMS_USERNAME;
  const password = process.env.CMS_PASSWORD;
  const authResponse = await client.authentication(name, password);
  if (!authResponse.ok) {
    return res.status(401).json({ message: 'Authentication faild.' });
  }

  // 未公開オブジェクトの取得
  const authData = await authResponse.json();
  const token = authData.access_token;
  const options = {
    basename: req.query.basename, // idを利用してもよいかもしれないと考えている
    cols: 'basename',
  };
  const response = await client.getObject('entry', null, 1, options, token); // 本稿ではモデルが固定になっている
  if (!response.ok) {
    return res.status(404).end();
  }

  // 情報を設定してリダイレクト処理
  const json = await response.json();
  res.setPreviewData({
    basename: json.basename,
    token: token, // 認証で取得したトークンを渡す
  });
  // Redirect to the path from the getObject.
  // Don't redirect to req.query.slug as that might lead to open redirect vulnerabilities.
  res.redirect(`/blog/${json.basename}`);
};

page/blog/[basename].jpはプレビュー情報を持つ場合と通常の場合で分岐を行い処理をします。大きな変更はなく、以下のようなコードになりました。

export const getStaticProps = async (context) => {
  let token = null;
  let options = {
    cols: 'title,text,excerpt,published_on',
  };

  if (context.preview) {
    // プレビューの場合
    options['basename'] = context.previewData.basename;
    token = context.previewData.token;
  } else {
    // 通常の場合
    options['basename'] = context.params.basename;
  }
  const response = await client.getObject('entry', null, 1, options, token);
  const entry = await response.json();
  return {
    props: {
        entry,
        isPreview: context.preview ? true : false,
    }
  }
};

コードをGitHubにPushしてAmplifyでビルドが完了したのですが、プレビューを実行するとCloudFrontが503エラーが発生しました。どうしたものかと考えたのですが、「WebサイトにCloudFront Functionを設定したら接続が503エラーとなったのでトラブルシュートしてみた | DevelopersIO」を参考にログを探して確認したところ、環境変数が上手く扱えていないようでした。「Amplify+Next.js 環境変数設定方法まとめ」を読んで知ったのですが、環境変数についてnext.config.jsに記述が必要でした。
CloudFrontのエラーをCloudWatchで確認している画面

プレビューが上手く行ったと思ったのですがそのままだと通常のページが正しく表示されなくなったので、プレビュー表示後すぐにブラウザ側で/api/preview?endpreview=1fetchしてプレビューデータを消すことにしました。

現在プレビュー対象モデルがentry固定になっているので、他のモデルにも対応させる方法を今後考えてみます。(クエリストリングで受け取るのだろうなと考えています。)