Contentful(たぶん待望の)新機能 “ライブプレビュー” がリリース! リアルタイムな記事確認&編集が可能になりました。

2023.06.08

ベルリンオフィスの小西です。

ヘッドレス CMS の Contentful からライブプレビュー機能がリリースされました! 記事執筆体験を向上させてくれる大きなアップグレードで、本記事では導入方法の紹介とともに、リポジトリ内容も自分なりに解説してみます。

これまで
フロントエンドを持たない(= ヘッドレスな)Contentfulでは、編集した記事が最終的にどんな見た目になるのか確認するために、フロントアプリケーションまで移動し、そちらの更新やビルドを待つ必要がありました。

これから
Contentful上の記事エディターと同じページ、かつリアルタイムで最終的なコンテンツの見た目をプレビューできるようになりました。コンテンツに変更を加えるとすぐにプレビューにも反映されます。

イメージとしては↓の感じです。

ライブプレビュー機能について

もう少し具体的な説明に移ります。

ライブプレビューは大きく3つの機能で構成されます。

1. 並列プレビュー&編集(Side-by-side previewing and editing)

一画面で Contentful エディタとフロントアプリを並行して表示する機能です。

指定した URL のフロントアプリケーションを Contentful の iframe 内に呼び出すことで実現しています。

こちらの機能は開発不要ですぐに利用し始めることができる分、↑に書いた以上の機能はなく、あくまでフロントの更新orビルドを待つ必要がある点に留意が必要です。

個人的に今回のアップデートの肝は次に紹介する 2&3 です。

2. ライブアップデート(Live updates)

Contentfulのエントリー(画面左側)を編集すると、フロントアプリケーションを更新しなくても、変更内容がプレビューペイン(画面右側)に同時に表示されます。

contentful live preview

プレビュー反映の待ち時間が必要なく、かつ最終的な見た目を確認しつつ記事編集ができるため、開発者・記事執筆者ともに待ち望まれていた機能かと思います。

エディタの種別に関係なく変更がほぼ一瞬で反映されるため、記事執筆のストレスが大幅に軽減されます。

後述するように SDK は JS で書かれており、コード側に記述されたフラグを利用して該当箇所の DOM を書き換えているのかな、と推測してます。

なお、プレビューに反映された変更は [Publish] しないと本番サイトには反映されない点は従来通り。

3. インスペクターモード(Inspector mode)

プレビューペイン(画面右側)の任意の部分にカーソルを合わせ、[編集] をクリックすると、そのソース フィールドにすばやくジャンプし、すぐに編集が開始できます。

contentful live preview

記事執筆者から開発者へのよくある質問「記事の○○の部分ってCMSのどこから編集すればいいの?」を解決してくれる機能です。執筆体験がより直感的になりますね。

ライブプレビューの実装・開発

1. 各機能の導入方法

機能 導入方法
1. 並列プレビュー&編集 開発不要 / コンソールからプレビューアプリURLを指定
2. ライブアップデート SDKによる開発/アプリケーション変更が必要
3. インスペクターモード SDKによる開発/アプリケーション変更が必要

2. 開発の前提

ライブプレビュー SDK は JavaScript で動作し、React に最適化されています。上記 Github には Next.js, Gatsby, Remix のサンプルも用意されています。

次に、開発が必要な 2 つの機能について詳細を説明します。

3. ライブプレビュー SDK の初期化

まず、Contentful とプレビューアプリの通信を確立するため、コンポーネントをラップするプロバイダ( ContentfulLivePreviewProvider )で SDK を初期化します。

import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react';

const App = ({ Component, pageProps }) => (
  <ContentfulLivePreviewProvider locale="en-US">
    <Component {...pageProps}>
  </ContentfulLivePreviewProvider>
)

プロバイダはオプションが利用できます。

<ContentfulLivePreviewProvider
  locale="set-your-locale-here" // 必須
  enableInspectorMode={false} // オプション。デフォルトは true 。
  enableLiveUpdates={false} // オプション。デフォルトは true 。
  debugMode={false} // オプション。デフォルトは false 。
>

enableInspectorModeenableLiveUpdates を OFF にするとライブプレビュー機能が無効化され、data-attributes のようなライブプレビューに利用される特定のデータ生成も行われなくなるようです。そのため本番環境などでは(環境変数などで)無効化しておく設定が推奨かと思われます。

4. ライブアップデート機能の実装

React でライブアップデートを設定するには useContentfulLiveUpdates() フックをインポートして、元のソースデータ(記事データなど)を渡します。フックは Contentful コンソール上でライブ更新されたデータを返します。

import { useContentfulLiveUpdates } from '@contentful/live-preview/react';
const updatedEntries = useContentfulLiveUpdates(entries);

また GraphQL においてライブアップデートのパフォーマンスを安定・向上させるためには、クエリを直接 useContentfulLiveUpdates() に渡すことが推奨されています。

const query = gql`
  query posts {
    postCollection(where: { slug: "${slug}" }, preview: true, limit: 1) {
      items {
        __typename
        sys {
          id
        }
        title
        content: description
      }
    }
  }
`

// ...
const updated = useContentfulLiveUpdates(originalData, { query })
// ...

5. インスペクターモードの実装

インスペクターモードを有効にするには、コード側の該当するフィールドにタギングを行います。タギングは該当の HTML タグにデータ属性を記述することで有効化され、ライブプレビュー SDK が要素をスキャンできるようになります。

下記の例では Text コンポーネントにタギング(属性付与)を行なっています。

export default function BlogPost: ({ blogPost }) {
  const inspectorProps = useContentfulInspectorMode()
  // Live updates for this component
  const data = useContentfulLiveUpdates(
    blogPost
  );

  return (
    <Section>
      <Heading as="h1">{data.heading}</Heading>
      <Text
        as="p"
        {...inspectorProps({
          entryId: data.sys.id,
          fieldId: 'text',
        })}>
        {data.text}
      </Text>
    </Section>
  );
}

また複数のフィールドのタギングを行う場合には、useContentfulInspectorMode フックに初期値を入れておくこともできます。

export default function BlogPost: ({ blogPost }) {
  const inspectorProps = useContentfulInspectorMode({ entryId: data.sys.id })

  return (
    <Section>
      <Heading as="h1" {...inspectorProps({ fieldId: 'heading' })}>{data.heading}</Heading>
      <Text as="p" {...inspectorProps({ fieldId: 'text' })}>
        {data.text}
      </Text>
    </Section>
  )

6. Contentful 側での有効化

Contentful 側の準備で必要なのは [Settings] → [Content preview] に進み、該当のモデルに対してプレビューアプリの URL を入力するのみです。 URL には変数が利用できます。詳しくはこちらから。

contentful preview

7. 実装例

私の Gatsby の個人ブログをベースに実装例を紹介します。なお Contentful との接続は gatsby-source-contentful で行なっています。

まずは初期化。

gatsby-browser.tsx

import React from 'react';
import { ContentfulLivePreviewProvider } from '@contentful/live-preview/react';
import '@contentful/live-preview/style.css';

export const wrapRootElement = ({ element }) => (
  <ContentfulLivePreviewProvider locale="en-US">
		{element}
	</ContentfulLivePreviewProvider>
);

次に記事ページ。

例えば Contentful でフィールド定義された title (短文テキストフィールド)と content (マークダウン長文フィールド)をレンダリングするための書き方として、下記の例があります。

Before

import React from 'react'
import { Link, graphql } from "gatsby";

const BlogPost = ({ data }) => {
  const post = data.contentfulBlogPost

  return (

      <div>
        <h1>
          {post.title || ''}
        </h1>
        {post.markdownContent
          && <div />
        }
      </div>

  );
};
export default BlogPost;

export const pageQuery = graphql`
  query( $slug: String) {
    contentfulBlogPost(slug: { eq: $slug }) {
      title
      markdownContent{
        childMarkdownRemark {
          html
        }
      }
    }
  }
`;

上記をライブプレビューに対応させるため、下記の記述に変更します(追記箇所はハイライトしました)。

After

import React from 'react';
import { graphql } from "gatsby";
import {
  useContentfulInspectorMode,
  useContentfulLiveUpdates,
} from '@contentful/live-preview/react';

const BlogPost = ({ data }) => {
  const post = data.contentfulBlogPost;
  const inspectorProps = useContentfulInspectorMode({ entryId: post.contentful_id });
  const updatedPost = useContentfulLiveUpdates({
    ...post,
    sys: { id: post.contentful_id },
  });

  return (
    <>
      <div>
        <h1
          {...inspectorProps({ fieldId: 'title' })}
        >
          {updatedPost.title || ''}
        </h1>

        {updatedPost.markdownContent
          && <div
              {...inspectorProps({ fieldId: 'markdownContent' })}
              dangerouslySetInnerHTML={{ __html: updatedPost.markdownContent.childMarkdownRemark.html }}
            />
        }

      </div>
    </>
  );
};
export default BlogPost;

export const pageQuery = graphql`
  query( $slug: String) {
    contentfulBlogPost(slug: { eq: $slug }) {
      __typename
      contentful_id
      title
      markdownContent{
        childMarkdownRemark {
          html
        }
      }
    }
  }
`;
  • useContentfulLiveUpdates() は渡された記事のオリジナルデータとユニーク ID を元に、Contentful 側で更新された記事データをリアルタイムで返します。
  • inspectorProps は、Contentful 側のインスペクターモードから該当のレンダリング箇所とフィールドを紐づけるための属性を付与しています。ここは画面上でのポインティングに利用されるため、ラップされていても問題ありません。
  • GraphQL クエリ に追加した __typenamecontentful_id は、Contentful がプレビュー記事との紐付けに利用するユニーク ID の役割を果たします。

注意点

認証クッキーなどの属性

iframe 内で実行されるライブプレビューにクッキーを渡すには SameSite=None; Secure フラグを設定します。例えば認証クッキーを使ってプレビューサイトにログインする場合、次のような記述になります:

Set-Cookie: auth=abc123; SameSite=None; Secure

セキュリティポリシーへの対応

ライブプレビュー画面で「接続を拒否されました」のメッセージが表示される場合、プレビューアプリ側のセキュリティ設定(セキュリティヘッダーまたはコンテンツセキュリティポリシー = CSP)が原因の可能性があります。

contentful live preview

その場合は下記の方法での回避をご検討ください。

  • X-Frame-Options header を取り除く
  • Content-Security-Policy ヘッダーに frame-ancestors https://app.contentful.com を追加する

最後に

以上、 Contentful(たぶん待望の)新機能であるライブプレビューを紹介してみました。

やはり肝心な点は、これが既存エディターの追加機能ではなく、Contentful のモデルから独立した新機能/SDKである点かと思います。そのため、記事モデルがどのようなフィールドもしくはエディター(カスタムアプリ含む)で構成されている場合でも、この機能は実装できるのが嬉しいところかと思います。

ヘッドレス CMS というコンセプトはあくまで開発者ファーストな表現だと思うので、今回のように純粋にスピーディで心地よい執筆体験を提供するアップデートがあると、編集者・執筆者サイドの方々にもより Contentful を勧めやすくなるなぁ、と感じました。

クラスメソッドでは Contentful の契約のご相談、構築支援をしています。ご興味のある方はぜひ弊社までお問い合わせください。

参考資料