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>
要素で囲まれるようになってしまいました。
以下のように記述すると、本来であれば同じ行に code1
と code2
が描画されてほしいのですが、それぞれ <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>
を配置しているため、上記の方法で置き換えることができません。

そこで、gatsby-plugin-mdx
を使わず、 @mdx-js/mdx のMDXファイルのコンパイル機能を直接使うことにしました。
MDXファイルのコンパイル結果(= コンポーネント)は、@mdx-js/mdx
の compile()
関数で得ることができます。
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にデプロイすることにしてこの問題を解消しました。
- actions/setup-node を使ってNode.js 18を使うようにしています
- プロジェクトIDなどの確認方法はhttps://github.com/vercel/vercel/discussions/4367を参考にしました
そのほか
gatsby-plugin-mdx
のノードの構造が変わったことで、いくつかのプラグイン(たとえばgatsby-plugin-feed
など)のコンフィグも変更する必要があるので対応しました。
まとめ
Gatsby関連のプラグインはGatsbyの更新とともに比較的頻繁に更新される雰囲気を感じており、Dependabotで一つずつライブラリを更新するより、手元で npm update
したほうが結果としては楽かもしれないなと思ったりしています。
ではでは~
クリックすると匿名でいいねできます。
(@piyoppi/counter-tools を使っています )