Tagbangers では新しい取り組みとして Web アプリケーションの構築の際に Headless CMS を用いたサイトデータ構築を行っています
デザインとデータを分けることで責務を分散させる他に開発者以外にもお客様が文言を直したり新しい項目を増やすことが可能になるので運用上のメリットを得られることができます
Headless CMS のサービスの中で弊社では Contentful を利用しており、現在も目下使い方や運用を考えながらアプリケーションの開発に携わっています
今回は Contentful で利用できる Rich Text とその拡張方法を試してみました
Contenful の Rich Text
Contentful では Content Type としてデータのモデリングを行い、実際のデータとして Entry を作成します
データはテキスト形式から日時データ、他の Content Type の参照などさまざまな形式がありますが、その中で Rich Text というタイプがあります
Rich Text ではエディタ内でスタイルを変更したり、リストやテーブルといった簡単な表現を Word ソフトのような使い勝手で記述できます
似た表現として Long Text タイプで Markdown を表現することができますが、下記の違いがあります
- Long Text
- Markdown の記述をサポートしたりプレビューするエディタを備えている
- Markdown のビューワーがあるだけで実態はただの Text 形式
- Markdown として描画するにはアプリケーション側の変換が必要
- Rich Text
- Rich Text エディタを備えている
- 項目ごとに Rich Text で利用できるスタイルの制限や個別のバリデーションが可能
- Entry を埋め込む ことができる
- 実態は JSON オブジェクト でスタイル形式なの情報がオブジェクトに含まれている
Rich Text は「Entry を埋め込む」ことで Rich Text で表現できないデータ表現を行えるようになります
React アプリにおける Rich Text の描画
npm に各種ライブラリがあります
contentful
を用いて Contentful から API で Entry を取得して、 @contentful/rich-text-react-renderer
を用いて React のコンポーネントに変換を行います Rich Text はスタイルごとに Node のタイプ ID が指定されているためその情報を列挙型として取得するために @contentful/rich-text-types
を利用します
Contentful API の利用方法は今回は省略します
@contentful/rich-text-react-renderer
は基本的に documentToReactComponents
に Rich Text のデータ(JSON)を渡すだけです
const Page = ({ entry }) => {
return <>{documentToReactComponents(entry.richTextData)}</>
}
デフォルトでは Rich Text のスタイルに対応する HTML タグに変換されます
- Heading 1 →
<h1>
- Normal Text →
<p>
注意点として、テーブル内やリスト内のテキストも <p>
で囲まれてしまうため意図せずスタイルが崩れる可能性があります
<ul>
<ui>
<p>リストアイテム</p>
</ui>
</ul>
先程の「Entry を埋め込み」に対する HTML タグはデフォルトでは用意されてないのでカスタムで用意する必要があります
const Page = ({ entry }) => {
return (
<>
{documentToReactComponents(document, {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node) => {
const contentTypeId = node.data.target.sys.contentType.sys.id
const entryFields = node.data.target.fields
switch (contentTypeId) {
case 'person':
return <PersonComponent {...entryFields}>
default:
return <>Unknown Component<>
}
}
},
})}
</>
)
}
documentToReactComponents
のオプションとして埋め込み Entry (BLOCKS.EMBEDDED_ENTRY
) のデータに対してその Content Type の ID ごとに対応する React Component を描画するよう指示を行います
renderNode
を拡張することで埋め込み Entry 以外にもデフォルトの変換を変えたりなどのカスタマイズが可能です
Contentful App Framework を用いた Rich Text のカスタマイズ
前置きが長くなりましたが、今回 Rich Text をカスタマイズした動機として「Rich Text で利用できるスタイルの種類を増やしたい」という理由がありました
Contentful で利用できるスタイルは少なく、足りないものは Content Type を定義して埋め込み Entry で表現する方法が基本です
ただし、頻繁に利用するスタイルに対して Entry を都度作成するのはメンテナンス性に影響があると考え、スタイルを拡張できないか調査を行いました
Contentful の Rich Text はスタイル制限はできるものの追加を行うことはまだできないようで、カスタムで拡張することが提案されています
Contentful では管理画面を拡張することができ、下記の2種類の手法があります
App Framework は後続でより開発・管理が楽になっているため今回はこちらを用いて、Heading スタイルに Caption
を追加する対応を行いました
Step1. Rich Text の Node Type を増やす
Contentful の RichText に関するライブラリは OSS で公開されているのでフォークしてカスタマイズします
BLOCKS.HEADING_6
の記述に沿って、BLOCKS.CAPTION
を追加しました
必要な他の対応も含めて下記の差分の対応を行なっています
npm で参照できるように GitHub Packages に publish しました
@koyama-tagbangers/rich-text-types
注意: GitHub Packages の利用は事前に準備が必要です
Step2. Rich Text の Editor を拡張する
Contentful の Rich Text エディタも OSS で公開されているため、フォークして先ほどの拡張した Node Type に対応させます
package.json
の参照先を変更し Heading の選択一覧に Caption
が出るよう定義を追加します
こちらも同じく GitHub Packages に publish しました
@koyama-tagbangers/field-editor-rich-text
Step3. Contentful App Framework を用いて Editor を登録
先程作った Rich Text Editor を Contentful で利用するために Contentful App を登録します
Create Contentful App を用いると簡単にセットアップからデプロイまで行うことができます
まずはローカルにプロジェクトを作成します
npx create-contentful-app custom-rich-text
プロジェクトは React + TypeScript のテンプレートとして作成されます
リポジトリ内で create-app-definition
スクリプトを実行して Organization に App を登録します
$ npm run create-app-definition
? App name (custom-rich-text): custom-rich-text
? Select where your app can be rendered: Entry field (entry-field)
? Select the field types the app can be rendered: JSON object
? Please paste your access token: [hidden]
? Select an organization for your app: My Organization
ここで注意点として Entry field / JSON object
の App として登録する 必要があります
field types のなかに Rich Text
がありますが、後述の理由によりカスタマイズしたものを利用できないため互換性のある JSON object
を選びます
完了すると .env
ファイルが出来上がり access token の情報などが登録されます
チームで共有したりリポジトリを公開する際はこのファイルは公開しないよう注意しましょう
次に先程の Rich Text Editor のパッケージをインストールして src/locations/Field.tsx
でコンポーネントを呼び出すよう編集をします
https://github.com/koyama-tagbangers/custom-rich-text/blob/main/src/locations/Field.tsx#L13
ビルドを行ったのち、 upload
スクリプトでデプロイを行います
npm run build
npm run upload
App は Contentful の Organization settings / App から詳細を閲覧できます
Step4. Contentful の Space に App をインストールして Content Type に組み込む
先程はあくまで Organization への登録で、利用するには Space(の Environment ごと)に App をインストールする必要があります
Space / Apps / Your custom apps に行くと登録した App が表示されているため3点ドットからインストールを選択します
Content Type の定義において JSON object の場合に Appearance タブに行くと登録した App が選べるようになっています
これで Contentful 上でカスタマイズした Rich Text が利用できるようになりました
Entry を作成してカスタムのスタイルで保存できることを確認しましょう
Step5. React アプリケーションでカスタマイズしたスタイルを描画できるようにする
埋め込み Entry と同じ要領でカスタマイズしたスタイルに対応するコンポーネントを実装します
const Page = ({ entry }) => {
return (
<>
{documentToReactComponents(document, {
renderNode: {
[BLOCKS.CAPTION]: (_, children) => {
return <Caption>{children}</Caption>
}
},
})}
</>
)
}
ただしスタイルの中身は、引数の2番目にあるため注意です
App Framework による Rich Text カスタマイズの問題点
- 簡単なスタイル追加でもコードの変更量が多い
- iframe で描画されるためウィンドウの高さやツールバーのスクロール固定など挙動が元の Rich Text と異なり、使用感が悪い
- フィールドタイプが JSON objects になるため、管理画面上でのスタイルの制限や個別のバリデーション設定を行うことができない
- 今後 Contentful や Rich Text の仕様が変わった際に App およびその依存ライブラリのメンテナンスが必要
JSON objects で定義する必要がある理由ですが、Contentful で Entry を送信する際に サーバ側でデフォルトで用意されている Node 以外の ID が含まれるとバリデーションエラーになり保存できない ためです
最初に説明したように Rich Text の実態は JSON objects のため互換性はありますが、反面 Rich Text 特有の管理機能が使えなくなるのは残念です
Rich Text は Contentful に導入されてからそれほど成熟されておらず、現在も活発に開発が続いているようです
今後スタイル追加が管理画面上から楽に行えるようになると嬉しいですね
Contentful は Slack のコミュニティがあり Rich Text 専用のチャンネルもあるため、気になった方は参加してみてください