TIPS: microCMSでリッチエディタのデータを扱うときは、HTML用のデータを一緒に定義すると捗る

## はじめに

プライベートで [microCMS](https://microcms.io/) と Next.js の組み合わせを多用しております。microCMS の良いところは、自由度の高いデータ定義ができることで、CMSというより、もはや使い勝手の良いデータストアサービスとして捉えています。

https://microcms.io/

今回はこの microCMS の一つのデータ型である、「リッチエディタ型」を扱う際のアイデアを一つ共有したいと思います。

---
追記
microCMS の公式さんに見つけていただけたようで、リプライいただきました。
本文部分をカスタムデータ(リッチエディタとプレーンテキスト)にして、繰り返して格納していくというアイデアです。確かにこちらの方がインタフェースとして直感的ですし、置換処理の必要がないので取り扱いやすいですね。
<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">ご要望ありがとうございます！<br>ご記載の内容ですとこの方法がご利用いただけそうです。<a href="https://t.co/0boMDy2iLy">https://t.co/0boMDy2iLy</a><br><br>とはいえリッチエディタのみでそのままHTMLを埋め込みたいというのもすごく理解できますので、検討させていただきます🙌</p>&mdash; microCMS｜編集者も使いやすい日本製ヘッドレスCMS (@micro_cms) <a href="https://twitter.com/micro_cms/status/1397344660062826496?ref_src=twsrc%5Etfw">May 26, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ちなみに、私のアイデアも、テンプレートとして同じコンテンツを何度も挿入を繰り返すというシーンにおいては、管理しやすいと思うので、このまま残させていただきたいと思います。

## モチベーション

**リッチエディタで編集したテキストデータに、任意のテンプレートHTMLを挿入することで、コンテンツの表現力を高める**

リッチエディタで編集したテキストは、自動的にHTMLに変換されます。
エスケープ処理せずにレンダリングすれば、そのままHTMLとして扱うことが可能です。(一般的なCMSと同じなので想像しやすいかと。)

実際にブログやLPを作って運用していると、CTA を設置したり、自身でカスタマイズしたスタイルで装飾したいときがたまにあります。
しかし HTML をリッチテキスト内に入力しても、テキストとしてサニタイズされてしまうため、レンダリングしても、そのまま表示されてしまい、意図した挙動にはなりません。

## 解決策

早速、解決策となる設定方法、実装方法を提示します。

### microCMS側の設定
まずはじめに、カスタムフィールド(ここでは shortCode)を定義します。
このカスタムデータは、データを識別するための短いテキスト(code)と、HTML本体(body)を持つように設定します。

- フィールドID: code | 表示名: 識別コード | 種類: テキストフィールド | 必須: true
- フィールドID: body | 表示名: HTML | 種類: テキストエリア | 必須: true

![unm7jg2ykoh8lntfvl5cbczrp6zb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46467/6133839e-81f7-0798-c6da-5903eb5e09fe.png)

次に、APIスキーマの方でポスト本体のデータを定義します。
本文以外のタイトルやアイキャッチ画像、メタ情報などはお好きに設定してください。
ここで、先程定義した shortCode を「繰り返しデータ」として定義します。

- フィールドID: body | 表示名: 本文 | 種類: リッチエディタ | 必須: true
- フィールドID: shortCodes | 表示名: ショートコード | 種類: 繰り返し(shortCode)
- それ以外のデータはお好きにどうぞ

![z3033mlbuwhgbv2mdnphgs25g7jc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46467/52fe3a37-e40e-6ede-3bd1-33eca8b825ee.png)


ここまでで、察しの良い方はこの先どうするか想像ができると思います。
そうです、shortCode に 任意のHTMLと識別コードを入力し、識別コードと同じ文字列を本文の方に記載していきます。

![スクリーンショット 2021-05-25 22.40.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46467/86c24dbf-0e1e-b7a1-8a17-76f40cfd30d4.png)


このように、本文中では識別コードの両サイドを `<< >>` や `[[ ]]` のような、実際の文章として発生しないような記号のパターンで囲ってあげてください。

> 識別コードの方では囲み文字を含まない形で定義したほうが良いです。
本文中の記号はエスケープされて保存されるので、のちの置換処理で手間が増えてしまいます。

### クライアント側のコード

こちらでは Next.js のコードを例に出したいと思います。
適宜自身が利用している言語やフレームワークに置き換えてください。
(複雑なことはしていないので、置き換えやすいと思います。)

上で定義したデータの型はこのような定義になっています。

```ts
// post.d.ts
export interface ShortCode {
  code: string
  body: string
}

export interface Post {
  id: string
  createdAt: string
  updatedAt: string
  publishedAt: string
  revisedAt: string
  title: string
  body: string
  shortCodes?: Array<ShortCode>
  // 省略
}
```

データ取得時に、本文から識別コードを正規表現でマッチさせ、対応するHTMLで置換させます。
> 前述の通り、識別コードの囲み記号はエスケープされていますので、一度コンソール等に出力させて確認してください。
`<< >>` => `&lt;&lt; &gt;&gt;`

```tsx
// pages/posts/[id].tsx

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const data = await client.get<Post>({
    endpoint: 'posts',
    contentId: params?.id ?? ''
  })
  return {
    props: {
      data: {
        ...data,
        body: replaceBody(data)
      }
    },
    revalidate: 3600
  }
}

export const replaceBody = ({ body, shortCodes }: Post): string => {
  const shortCodesMap =
    shortCodes?.reduce<Record<string, string>>(
      (res, { code, body }) => ({ ...res, [code]: body }),
      {}
    ) ?? {}
  return body
    .replace(/&lt;&lt;(.+?)&gt;&gt;/g, (...[, key]) => shortCodesMap[key])
}

interface PageProps {
  data: Post
}

const Page: FC = ({ data }) => {
  return (
    <main>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.body }} />
    </main>
  )
}

export default Page
```

### 実行結果
赤枠で囲った部分が、shortCode によって置換された場所です。
![スクリーンショット 2021-05-25 22.39.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46467/4be28ea3-4e84-cb02-b1e4-155379a2a8c0.png)



## その他

**任意のHTMLを埋め込み可能であるということは、インジェクションの原因になりますので、使用される際には十分に注意してください。**

今回の例ではわかりやすいように、ポストデータに shortCode を内包するデータの形にしています。実際には、shortCode を共有パーツ化して使用したいケースの方が多いと思いますので、テーブルを別定義して管理した方が、使い勝手が良くなると思います。

また、置換用の HTML は microCMS で管理せずに、`html-react-parser` などと組み合わせて、フレームワーク側で定義したコンポネントと置換するようにすると、より開発しやすくなります。
https://www.npmjs.com/package/html-react-parser

P.S.
もし microCMS の運営さんが見られておりましたら、ぜひショートコードないし、リッチエディターで編集したテキストに、任意のHTMLを挿入できる機能の導入を検討していただけますと幸いです。ぜひご検討の程よろしくおねがいします。:relaxed: