Tagbangers Blog

Contentful App Framework を用いて RichText を拡張する

Tagbangers では新しい取り組みとして Web アプリケーションの構築の際に Headless CMS を用いたサイトデータ構築を行っています

デザインとデータを分けることで責務を分散させる他に開発者以外にもお客様が文言を直したり新しい項目を増やすことが可能になるので運用上のメリットを得られることができます

Headless CMS のサービスの中で弊社では Contentful を利用しており、現在も目下使い方や運用を考えながらアプリケーションの開発に携わっています

今回は Contentful で利用できる Rich Text とその拡張方法を試してみました

Contenful の Rich Text

Contentful では Content Type としてデータのモデリングを行い、実際のデータとして Entry を作成します

Content modeling basics

データはテキスト形式から日時データ、他の Content Type の参照などさまざまな形式がありますが、その中で Rich Text というタイプがあります

Rich Text

Rich Text ではエディタ内でスタイルを変更したり、リストやテーブルといった簡単な表現を Word ソフトのような使い勝手で記述できます

似た表現として Long Text タイプで Markdown を表現することができますが、下記の違いがあります

  • Long Text
    • Markdown の記述をサポートしたりプレビューするエディタを備えている
    • Markdown のビューワーがあるだけで実態はただの Text 形式
    • Markdown として描画するにはアプリケーション側の変換が必要
  • Rich Text
    • Rich Text エディタを備えている
    • 項目ごとに Rich Text で利用できるスタイルの制限や個別のバリデーションが可能
    • Entry を埋め込む ことができる
    • 実態は JSON オブジェクト でスタイル形式なの情報がオブジェクトに含まれている
Long Text
Rich Text

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 はスタイル制限はできるものの追加を行うことはまだできないようで、カスタムで拡張することが提案されています

Additional text styles #56

Contentful では管理画面を拡張することができ、下記の2種類の手法があります

App Framework は後続でより開発・管理が楽になっているため今回はこちらを用いて、Heading スタイルに Caption を追加する対応を行いました

Step1. Rich Text の Node Type を増やす

Contentful の RichText に関するライブラリは OSS で公開されているのでフォークしてカスタマイズします

contentful / rich-text

BLOCKS.HEADING_6 の記述に沿って、BLOCKS.CAPTION を追加しました

必要な他の対応も含めて下記の差分の対応を行なっています

https://github.com/koyama-tagbangers/rich-text/commit/0032a9d3f439f6de9e4ed55bb1ae1870f7a685b7#diff-dcc2464e2be3a093734f36c0fe10650bf80a4f833707f62561c6cd007490cb0c

npm で参照できるように GitHub Packages に publish しました

@koyama-tagbangers/rich-text-types

注意: GitHub Packages の利用は事前に準備が必要です

Working with the npm registry

Step2. Rich Text の Editor を拡張する

Contentful の Rich Text エディタも OSS で公開されているため、フォークして先ほどの拡張した Node Type に対応させます

contentful / field-editors

package.json の参照先を変更し Heading の選択一覧に Caption が出るよう定義を追加します

https://github.com/koyama-tagbangers/field-editors/commit/f295ba11ee695f58730dd31e5f5fc0c52bb1a22a#diff-3d756c1ed0f403e2303b3a54fcd1b6ce8001dda6b940abf55450557093ed47da

こちらも同じく 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 専用のチャンネルもあるため、気になった方は参加してみてください

Contentful Community Slack