Gatsby(GatsbyJS)×Contentful×リッチテキストでコードのハイライト表示

Webサイト高速化のための 静的サイトジェネレーター活用入門を通じて作成したこのブログにはコードを載せているので、コードブロックがいい感じにシンタックスハイライト表示されるようにしたいと思っていました。
検索するとGatsby(GatsbyJS)ではプラグイン「gatsby-remark-prismjs」を使う方法が当然のように紹介されています。
しかし、このブログはContentfulのリッチテキストで入力しているため、マークダウンの利用が前提となっている「gatsby-remark-prismjs」を使うことはできません。

調べて試した結果、「react-syntax-highlighter」を使うことで対応できました!

すべては参考にした以下サイトのおかげです。

▼Code Snippets with Contentful | Christian Coda
https://www.christiancoda.com/blog/code-snippets-with-contentful/

▼gatsby - How to format code snippets with <pre> tags using Contentful's rich-text-react-renderer? - Stack Overflow
https://stackoverflow.com/questions/57149824/how-to-format-code-snippets-with-pre-tags-using-contentfuls-rich-text-react-r

react-syntax-highlighterをインストール

書籍のやり方に則って、yarnでインストールします。

yarn add react-syntax-highlighter

npmの場合は以下の通り。

npm install react-syntax-highlighter

blogpost-template.jsの編集

MARKSを使えるように定義します。

// 変更前
import { BLOCKS } from "@contentful/rich-text-types"

// 変更後
import { BLOCKS, MARKS } from "@contentful/rich-text-types"

react-syntax-highlighterを使えるようにimportします。
シンタックスハイライトのスタイルはokaidiaを指定しています。
スタイルはお好みで変更しても良いと思います。

// コードブロックのシンタックスハイライト
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { okaidia } from "react-syntax-highlighter/dist/cjs/styles/prism";

デフォルトの状態でコードブロックを表示すると<p>タグで括られてしまうため構文として正しくなく、ブラウザのConsoleを表示するとエラーが表示されます。
<p>タグではなく、汎用的な<div>タグで括るように変更します。

/* こんな状態から */
<p>
  <code>
    console.log(hoge);
  </code>
</p>

/* こうしたい */
<div>
  <code>
    console.log(hoge);
  </code>
</div>

optionsの[BLOCKS.EMBEDDED_ASSET]...の次あたりに、[BLOCKS.PARAGRAPH]...を追記します。
続きで、renderMark...も追記しておきます。

// 変更前
  renderNode: {
    ...
    ...
    [BLOCKS.EMBEDDED_ASSET]: node => (
      <Img
        fluid={useContentfulImage(node.data.target.fields.file["ja-JP"].url)}
        alt={
          node.data.target.fields.description
            ? node.data.target.fields.description["ja-JP"]
            : node.data.target.fields.title["ja-JP"]
        }
      />
    )
  },

// 変更後
  renderNode: {
    ...
    ...
    [BLOCKS.EMBEDDED_ASSET]: node => (
      <Img
        fluid={useContentfulImage(node.data.target.fields.file["ja-JP"].url)}
        alt={
          node.data.target.fields.description
            ? node.data.target.fields.description["ja-JP"]
            : node.data.target.fields.title["ja-JP"]
        }
      />
    ),
    // コードブロックをdivで括る
    [BLOCKS.PARAGRAPH]: (node, children) => {
      if (
        node.content.length === 1 &&
        node.content[0].marks.find((x) => x.type === "code")
      ) {
        return <div>{children}</div>;
      }
      return <p>{children}</p>;
    },
  },
  // コードブロック
  renderMark: {
    [MARKS.CODE]: code,
  },

const options = {...} の次あたりに、SyntaxHighlighterの実行処理を追記します。

// コードブロックのシンタックスハイライト
function code(text) {
  text.shift(); // コードブロックのfalseを削除
  const language = text.shift(); // コードブロックの1行目の言語指定をClassに利用後削除
  text.shift(); // コードブロックの1行目の改行を削除

  const value = text.reduce((acc, cur) => {
    if (typeof cur !== "string" && cur.type === "br") {
      return acc + "\n";
    }
    return acc + cur;
  }, "");

  return (
    <SyntaxHighlighter language={language} style={okaidia}>
      {value}
    </SyntaxHighlighter>
  );
}

使用方法

コードブロックの1行目に、何の言語か指定するために文字列を記載します。

JavaScript

javascript
console.log('1行目でjavascriptを定義');

HTML

html
<p>1行目でhtmlを定義</p>

CSS

css
/* 1行目でcssを定義 */
.hoge{color:red;}