⚒️

Next.js・ヘッドレスCMS|マークダウンしたHTML文字列をReact要素に変換

    ヘッドレスCMS
    Next.js
    React.js

実現したいこと・何を活用するか

NewtやMicro CMSなんかで構築していると、マークダウン形式で受け取った文字列のHTML要素を、DOM要素に変換したい時があると思います。さらに、Next.jsで開発をしていると、next/imageでの画像最適化やモジュールCSSのスタイリングなど、Next.jsの恩恵を最大限活かしたいものです。

そこで下記を活用しつつ、Next.jsの開発に最適化した方法を試したので開発メモとして残しておきます!

  • html-react-parser によるHTML文字列からReactElementへの変換
  • optionsreplaceメソッドを使った最適化

簡単な説明になりますが、少しでも参考になれば幸いです!

html-react-parser によるHTML文字列からReact要素への変換

html-react-parserとはHTML文字列をReact要素へ変換できるパッケージで、サーバー上(node.js)でもクライアント上(browser)でも動かすことができます。

html-react-parserのGitHubページ

GitHubに記載の通り、最小限の構成だと下記で試すことができます。

#NPM
npm install html-react-parser --save

# Yarn
yarn add html-react-parser
import parse from 'html-react-parser';
parse('<p>Hello, World!</p>'); 
// React.createElement('p', {}, 'Hello, World!')

デフォルトでインポートした関数の第1引数に、文字列のHTMLを投げれば、React.createElementの結果が返ってくる形になります。

例えば、ヘッドレスCMSでマークダウンで記述したブログの本文なんかを、下記のように受け取ってNext.jsに組み込むことができます。

/*
ヘッドレスCMSからデータを取得する流れは省略
`body`という変数に本文(HTML文字列)を格納しているものとします
*/
import parse from 'html-react-parser';

export default function BlogBody({
  body
}: {
  body: string
}) {
  return <>
    <h1>
      ブログ本文
    </h1>
    {parse(body)}
  </>
}

parse()の戻り値は下記添付のように、React.createElement()で作成したReact要素を詰め込んだ配列となっているので、そのままJSX構文に組みことができます。

img-tech-blog-urg7wh5yxo0fdvclh2gh.jpg

optionsreplaceメソッドを使った最適化

parse関数の第2引数にはオプションを詰めることができて、そのうちのひとつに、React要素へ変換する途中で任意の処理を施すことができるreplaceメソッドがあります。

例えば、<table>タグを任意のクラスを付与した<div>タグで囲む例を書いてみます。

import parse, { attributesToProps, DOMNode, domToReact } from 'html-react-parser';

export default function BlogBody({
  body
}: {
  body: string
}) {
  const options: HTMLReactParserOptions = {
    replace(domNode) {
      if (!(domNode instanceof Element)) return;
      if (domNode.name === 'table') {
        // tableノードかどうかを特定
        return <div className='myTable'>
          <table>
            {domToReact(domNode.children as DOMNode[], options)}
            {/* domToReactで元々の子要素を挿入 */}
          </table>
        </div>
      }
    }
  }
  return <>
    <h1>
      ブログ本文
    </h1>
    {parse(body, options)}
  </>
}

例えば、上記のbody<table> ... [省略] ... </table>というHTML文字列が渡っていたとしたら、最終的に下記のような形でレンダリングされます。

<div class="myTable">
  <table>
    ... [省略] ...
  </table>
</div>

他にも下記のような操作が可能です。

  • 特定のHTML属性を持つ要素にクラスを付与
  • 特定したHTMLタグの末尾に自分で定義したReactコンポーネントを差し込む

例えば、外部リンクの<a>タグに、<ExternalLink />という独自定義したReactコンポーネントを差し込むならこんな感じになります。

if (domNode.name === 'a' && domNode.attribs.href.includes('http')) {
  const props = attributesToProps(domNode.attribs);
  return <a {...props} className={`${base.externalLink}`}>
    {domToReact(domNode.children as DOMNode[], options)}
    <ExternalLink />
  </a>
}

外部リンクかどうかの判定は簡易的なものですが...特定の属性値が含まれているかどうかの判定ができて、既存の属性値全てを継承することができますし、任意の要素を差し込むこともできます。

公式ドキュメントが非常に分かりやすいので、詳しくは下記をご参照ください...!

html-react-parserのGitHubページ

よくあるエラー

ちなみに、TypeScriptでdomToReactを使おうとするとArgument of type 'ChildNode[]' is not assignable to parameter of type 'DOMNode[]'.というエラーに出くわすと思いますが、下記の通りに型定義してやれば解決できます。

# If you're getting the error:

# Argument of type 'ChildNode[]' is not assignable to parameter of type 'DOMNode[]'.
# Then use type assertion:

domToReact(domNode.children as DOMNode[], options);

html-react-parserのGitHub|Migration