ベルリンオフィスの小西です。
ヘッドレス CMS の Contentful からライブプレビュー機能がリリースされました! 記事執筆体験を向上させてくれる大きなアップグレードで、本記事では導入方法の紹介とともに、リポジトリ内容も自分なりに解説してみます。
これまで
フロントエンドを持たない(= ヘッドレスな)Contentfulでは、編集した記事が最終的にどんな見た目になるのか確認するために、フロントアプリケーションまで移動し、そちらの更新やビルドを待つ必要がありました。
↓
これから
Contentful上の記事エディターと同じページ、かつリアルタイムで最終的なコンテンツの見た目をプレビューできるようになりました。コンテンツに変更を加えるとすぐにプレビューにも反映されます。
イメージとしては↓の感じです。
ライブプレビュー機能について
もう少し具体的な説明に移ります。
ライブプレビューは大きく3つの機能で構成されます。
1. 並列プレビュー&編集(Side-by-side previewing and editing)
一画面で Contentful エディタとフロントアプリを並行して表示する機能です。
指定した URL のフロントアプリケーションを Contentful の iframe 内に呼び出すことで実現しています。
こちらの機能は開発不要ですぐに利用し始めることができる分、↑に書いた以上の機能はなく、あくまでフロントの更新orビルドを待つ必要がある点に留意が必要です。
個人的に今回のアップデートの肝は次に紹介する 2&3 です。
2. ライブアップデート(Live updates)
Contentfulのエントリー(画面左側)を編集すると、フロントアプリケーションを更新しなくても、変更内容がプレビューペイン(画面右側)に同時に表示されます。
プレビュー反映の待ち時間が必要なく、かつ最終的な見た目を確認しつつ記事編集ができるため、開発者・記事執筆者ともに待ち望まれていた機能かと思います。
エディタの種別に関係なく変更がほぼ一瞬で反映されるため、記事執筆のストレスが大幅に軽減されます。
後述するように SDK は JS で書かれており、コード側に記述されたフラグを利用して該当箇所の DOM を書き換えているのかな、と推測してます。
なお、プレビューに反映された変更は [Publish] しないと本番サイトには反映されない点は従来通り。
3. インスペクターモード(Inspector mode)
プレビューペイン(画面右側)の任意の部分にカーソルを合わせ、[編集] をクリックすると、そのソース フィールドにすばやくジャンプし、すぐに編集が開始できます。
記事執筆者から開発者へのよくある質問「記事の○○の部分ってCMSのどこから編集すればいいの?」を解決してくれる機能です。執筆体験がより直感的になりますね。
ライブプレビューの実装・開発
1. 各機能の導入方法
機能 | 導入方法 |
---|---|
1. 並列プレビュー&編集 | 開発不要 / コンソールからプレビューアプリURLを指定 |
2. ライブアップデート | SDKによる開発/アプリケーション変更が必要 |
3. インスペクターモード | SDKによる開発/アプリケーション変更が必要 |
2. 開発の前提
- Node.js:
>=16.15.1
- SDK: https://github.com/contentful/live-preview
ライブプレビュー 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 。
>
enableInspectorMode
や enableLiveUpdates
を 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 には変数が利用できます。詳しくはこちらから。
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 クエリ に追加した
__typename
とcontentful_id
は、Contentful がプレビュー記事との紐付けに利用するユニーク ID の役割を果たします。
注意点
認証クッキーなどの属性
iframe 内で実行されるライブプレビューにクッキーを渡すには SameSite=None; Secure
フラグを設定します。例えば認証クッキーを使ってプレビューサイトにログインする場合、次のような記述になります:
Set-Cookie: auth=abc123; SameSite=None; Secure
セキュリティポリシーへの対応
ライブプレビュー画面で「接続を拒否されました」のメッセージが表示される場合、プレビューアプリ側のセキュリティ設定(セキュリティヘッダーまたはコンテンツセキュリティポリシー = CSP)が原因の可能性があります。
その場合は下記の方法での回避をご検討ください。
X-Frame-Options header
を取り除く- Content-Security-Policy ヘッダーに
frame-ancestors https://app.contentful.com
を追加する
最後に
以上、 Contentful(たぶん待望の)新機能であるライブプレビューを紹介してみました。
やはり肝心な点は、これが既存エディターの追加機能ではなく、Contentful のモデルから独立した新機能/SDKである点かと思います。そのため、記事モデルがどのようなフィールドもしくはエディター(カスタムアプリ含む)で構成されている場合でも、この機能は実装できるのが嬉しいところかと思います。
ヘッドレス CMS というコンセプトはあくまで開発者ファーストな表現だと思うので、今回のように純粋にスピーディで心地よい執筆体験を提供するアップデートがあると、編集者・執筆者サイドの方々にもより Contentful を勧めやすくなるなぁ、と感じました。
クラスメソッドでは Contentful の契約のご相談、構築支援をしています。ご興味のある方はぜひ弊社までお問い合わせください。
参考資料
- https://www.contentful.com/developers/docs/tutorials/general/live-preview/
- Live preview に関する概要説明
- https://github.com/contentful/live-preview
- Live Preview の SDK のリポジトリ