Gatsbyベースのブログ環境のライブラリアップデート 2022年秋

ひさびさにブログでも書こうと思ったら、ブログ環境を壊してしまっていることに気がつきました。

このブログ環境はGatsbyを利用しており、 piyoppi/piyoppi.github.io をフォークして開発しています。 依存ライブラリの更新にDependabotを活用しておりますが、まじめに変更差分をチェックせずにDependabotが作ってくれるPull Requestをポチポチとマージしてしまっていたのでした。

破壊的変更を含むライブラリ更新も含まれていたようで、動かなくなってしまっていたのでした。

ということで直していきます。

gatsby-plugin-mdx

MDXをレンダリングするために gatsby-plugin-mdx を利用しておりましたが、 バージョン4以降は MDX v2 互換となった影響で破壊的変更が入っています。 マイグレーションガイド に従って作業します。

remark-gfm の導入

表などのマークダウン表記は GitHub Flavored Markdown に基づくものですが、 gatsby-plugin-mdx バージョン4以降は remark-gfm を別途インストールしないと <table> 要素に変換されません。

remark-gfm をインストールしたうえで、以下の差分を gatsby-config.js に追加します。

    plugins: [
      {
        resolve: "gatsby-plugin-mdx",
        options: {
+        mdxOptions: {
+          remarkPlugins: [
+            require('remark-gfm')
+         ],
        },
      },
    ],
  }

gatsby-remark-prismjs の利用をやめて @mapbox/rehype-prism を使う(様子見)

最新の gatsby-remark-prismjs をインストールすると、インライン要素として表示してほしい <code> 要素がブロック要素である <div> 要素で囲まれるようになってしまいました。

以下のように記述すると、本来であれば同じ行に code1code2 が描画されてほしいのですが、それぞれ <div> タグで囲われてしまい、同じ行に表示されません。

`code1` `code2`

原因は結局わからずなのですが、ひとまず MDX - Syntax higilighting で解説されている @mapbox/rehype-prism を利用することとして回避しました。

@mapbox/rehype-prism をインストールしたうえで、以下の差分を gatsby-config.js に追加します。

    plugins: [
      {
        resolve: "gatsby-plugin-mdx",
        options: {
          mdxOptions: {
+           rehypePlugins: [
+             require('@mapbox/rehype-prism')
+           ]
          }
        },
      },
    ],
  }

しばらくはこれで様子を見ようと思います。

ブログ記事ページの <MDXRenderer> の置き換え

※詳細は公式ドキュメントのマイグレーションガイドをご覧ください。

これまでは、MDXフォーマットのテキストを <MDXRenderer> タグで囲むことで描画していました。

export default function BlogPost({ data }) {
  return (
    // data.mdx.body には mdxファイルのテキストが入っている
    <MDXRenderer>
      { data.mdx.body }
    </MDXRenderer>
  )
}

export const query = graphql`
  query ($id: String) {
    mdx(id: {eq: $id}) {
      frontmatter {
        title
      }
      body
    }
  }`

v4以降は <MDXRenderer> が廃止されたようなので、マイグレーションガイドに従い、ページコンポーネントを以下のように修正します。

- export default function BlogPost({ data }) {
+ export default function BlogPost({ children }) {
    return (
-     <MDXRenderer>
-       { data.mdx.body }
+       { children }
-     </MDXRenderer>
    )
  }
  
  export const query = graphql`
    query ($id: String) {
      mdx(id: {eq: $id}) {
        frontmatter {
          title
        }
        body
      }
    }`

また、 gatsby.config.js を以下のように修正します。createPage の際のページコンポーネントのパスに ?__contentFilePath クエリパラメータをつけることで、ページコンポーネントに渡されるオブジェクトに、上記のように children (コンパイル済みMDXコンポーネント)が追加されるようです。

  exports.createPages = async ({ graphql, actions }) => {
    const posts = await graphql(`
      query {
-       allMdx(filter: {fileAbsolutePath: {regex: "/posts/"}}) {
+       allMdx(filter: {internal: {contentFilePath: {regex: "/posts/"}}}) {
          nodes {
            id
-           slug
+           fields {
+             slug
+           },
+           internal {
+             contentFilePath
+           }
          }
        }
      }
    `)
  }

  posts.data.allMdx.nodes.forEach(param => {
    createPage({
      path: `/weblog/${param.fields.slug}`,
-     component: path.resolve(`./src/templates/post.js`),
+     component: path.resolve(`./src/templates/post.js`) + `?__contentFilePath=${param.internal.contentFilePath}`,
      context: {
        id: param.id,
      },
    })
  })

一つのページに複数の <MDXRenderer> を利用している場合の置き換え

一方で、トップページ にあるカード(たとえば これ)では、一つのページに複数の <MDXRenderer> を配置しているため、上記の方法で置き換えることができません。

トップページに複数のMDXRendererが存在するようす

そこで、gatsby-plugin-mdx を使わず、 @mdx-js/mdx のMDXファイルのコンパイル機能を直接使うことにしました。 MDXファイルのコンパイル結果(= コンポーネント)は、@mdx-js/mdxcompile() 関数で得ることができます。

gatsby-node.js を以下のように編集します。

exports.onCreateNode = async ({ node, actions, getNode }) => {
  const { createNodeField } = actions
  const { compile } = await import('@mdx-js/mdx')
  const remarkFrontmatter = (await import('remark-frontmatter')).default

  if (node.internal.type === 'Mdx') {

    // ... (省略) ...

    // /top/cards 以下のmdxのみをビルド対象とする

    if (node.internal.contentFilePath.includes('/top/cards/')) {
      const mdx = fs.readFileSync(node.internal.contentFilePath)

      // MDXファイルのコンパイル
      //
      // - function-body を指定することで、 compiled にはコンパイル結果の
      //   ReactコンポーネントのRender関数が代入される
      // - 処理対象のMDXファイルにはfrontmatterが含まれるので
      //   remarkFrontmatter プラグインを適用する
      const compiled = await compile(
        mdx,
        {
          outputFormat: 'function-body',
          providerImportSource: '@mdx-js/react',
          remarkPlugins: [remarkFrontmatter]
        }
      )

      // コンパイル結果のコンポーネント(JavaScript文字列)をノードに含める
      createNodeField({
        name: `compiled`,
        node,
        value: compiled.value
      })
    } else {
      createNodeField({
        name: `compiled`,
        node,
        value: ''
      })
    }
  }
}

ページコンポーネントでは以下のように記述します。ノードに含めたコンパイル結果をページコンポーネントのレンダリング関数内部で runSync() 関数を使って評価し、コンポーネントを描画します。

import { runSync } from '@mdx-js/mdx'
import { Fragment, jsx, jsxs } from 'react/jsx-runtime'
import { useMDXComponents } from '@mdx-js/react'

export default function Home() {
  // fields.compiled も取得する
  const data = useStaticQuery(graphql`
    {
      allMdx(
        filter: { frontmatter: { page: { eq: "/" } } }
      ) {
        nodes {
          frontmatter {
            title
          }
          fields {
            compiled
          }
        }
      }
    }
  `)

  return (
    {data.allMdx.nodes.map(card => (
      <h2>{card.frontmatter.title}</h2>

      // card.fields.compiled にはMDXのコンパイル結果(ReactコンポーネントのJavaScript文字列)が
      // 代入されている
      // runSync を使うことでこれを評価し、レンダリングする
      {runSync(card.fields.compiled, {Fragment, jsx, jsxs, useMDXComponents}).default()}
    ))}
  )
}

これで単一ページに複数のMDXファイルを含む場合に対応できました。

Vercelへのデプロイスクリプト

このブログはVercelでホスティングされています。

Gatsby 5系はNode 18が必要なようなのですが、Vercel側のNode.js バージョンは 14系と16系しか選べません。試しにデプロイしてみると Gatsby requires Node.js 18.0.0 or higher というエラーメッセージとともにビルドに失敗してしまいます。

How can I use GitHub Actions with Vercel? を参考に、GitHub Actions側でビルドしてVercelにデプロイすることにしてこの問題を解消しました。

そのほか

gatsby-plugin-mdxのノードの構造が変わったことで、いくつかのプラグイン(たとえばgatsby-plugin-feedなど)のコンフィグも変更する必要があるので対応しました。

まとめ

Gatsby関連のプラグインはGatsbyの更新とともに比較的頻繁に更新される雰囲気を感じており、Dependabotで一つずつライブラリを更新するより、手元で npm update したほうが結果としては楽かもしれないなと思ったりしています。

ではでは~

このカウンタは @piyoppi/counter-tools を使っています。

クリックすると匿名でいいねできます。