Gatsby Starter BlogでMDXを使用できるようにする

Gatsby Starter BlogはMarkdown(.md)で記事が書けるようになっているが、 簡単な分、複雑なことをするには向いてない。そこでMDXを使用して記事を書けるように変更する。

MDXとは

MDXとは、Markdown文書の中にJSXも書ける形式のこと。拡張子はmdx。 つまりReactコンポーネントを使って記事を書けたりするので、 普通のMarkdownではできない(めんどくさい)処理を書いたりすることもできる。 Gatsby開発チームの人が作ってるのでGatsby製サイトとの親和性は高いが、Next.jsなどGatsby以外でも使える。

参照元

Gatsbyの公式ブログでMDXに替える方法が丁寧に説明されているので大体はこれに従う。

How to convert an existing Gatsby blog to use MDX

導入手順

必要なパッケージの導入

MDXを使用するために必要なパッケージを以下コマンドで導入する。

Shell
npm install gatsby-plugin-mdx gatsby-plugin-feed-mdx @mdx-js/mdx @mdx-js/react

次に、gatsby-config.jsを変更する。

  • gatsby-transformer-remarkgatsby-plugin-mdxに置き換える
    • オプションをgatsby-plugin-mdxに合わせて書き換える&追加する
  • gatsby-plugin-feedgatsby-plugin-feed-mdxに置き換える
gatsby-config.js
module.exports = {
  // ...
  plugins: [
    // ...
    {
-     resolve: `gatsby-transformer-remark`,
+     resolve: `gatsby-plugin-mdx`,
      options: {
+       extensions: [`.mdx`, `.md`],
-       plugins: [
+       gatsbyRemarkPlugins: [
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 630,
            },
          },
          // ...
        ]
      }
    },
-   `gatsby-plugin-feed`,
+   `gatsby-plugin-feed-mdx`,
    // ...
  ]
}

不要になるパッケージの削除

MDXを使用する方法に変更するため、元々導入されていたMarkdown読み込み用のパッケージは削除する。

Shell
npm uninstall gatsby-transformer-remark gatsby-plugin-feed

ビルド処理の修正

プラグイン変更に伴い、以下の書き換えが必要になる。

  • allMarkdownRemarkallMdx
  • MarkdownRemarkMdx
  • markdownRemarkmdx
  • GraphQLで取得するフィールドをhtmlからbody
  • 記事の展開をMDXRenderer

書き換え対象は以下。

  • gatsby-node.js
  • src/pages/index.js
  • src/templates/blog-post.js

順に修正していく。

gatsby-node.js

Gatsby Starter Blogなら4箇所あるはず。

gatsby-node.js
exports.createPages = async ({ graphql, actions, reporter }) => {
  // ...
  const result = await graphql(
    `
      {
-       allMarkdownRemark(
+       allMdx(
          ...
    `
  // ...

- const posts = result.data.allMarkdownRemark.nodes
+ const posts = result.data.allMdx.nodes

  // ...

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions
- if (node.internal.type === `MarkdownRemark`) {
+ if (node.internal.type === `Mdx`) {
    const value = createFilePath({ node, getNode })

  // ...

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  createTypes(`
    // ...
-   type MarkdownRemark implements Node {
+   type Mdx implements Node {
      frontmatter: Frontmatter
      fields: Fields
    }
  ...
  `
  // ...

src/pages/index.js

Gatsby Starter Blogなら2箇所あるはず。

src/pages/index.js
// ...
const BlogIndex = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`
- const posts = data.allMarkdownRemark.nodes
+ const posts = data.allMdx.nodes
  // ...
}

// ...

export const pageQuery = graphql`
  query {
    ...
-   allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC })
+   allMdx(sort: { fields: [frontmatter___date], order: DESC })
    ...
`

src/templates/blog-post.js

Gatsby Starter Blogなら7箇所あるはず。

src/templates/blog-post.js
// ...
+ import { MDXRenderer } from "gatsby-plugin-mdx"

const BlogPostTemplate = ({ data, location }) => {
- const post = data.markdownRemark
+ const post = data.mdx

  // ...

  return (
    <Layout location={location} title={siteTitle}>
      // ...
      <article
        className="blog-post"
        itemScope
        itemType="http://schema.org/Article"
      >
        <header>
          <h1 itemProp="headline">{post.frontmatter.title}</h1>
          <p>{post.frontmatter.date}</p>
        </header>
 -       <section
-         dangerouslySetInnerHTML={{ __html: post.html }}
-         itemProp="articleBody"
-       />
+       <MDXRenderer itemProp="articleBody">{post.body}</MDXRenderer>

// ...

export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
    $previousPostId: String
    $nextPostId: String
  ) {
    site {
      siteMetadata {
        title
      }
    }
-   markdownRemark(id: { eq: $id }) {
+   mdx(id: { eq: $id }) {
      id
      excerpt(pruneLength: 160)
-     html
+     body
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
      }
    }
-   previous: markdownRemark(id: { eq: $previousPostId }) {
+   previous: mdx(id: { eq: $previousPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
-   next: markdownRemark(id: { eq: $nextPostId }) {
+   next: mdx(id: { eq: $nextPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
  }
`

動作の確認

上記の設定を行ったら、MDXを使用できるようになっている。 例えば以下のようなmdxファイルで記事を作成してgatsby developしてみる。

content/blog/example.mdx
---
title: MDX!
date: 2021-06-09
description: "A post showing MDX in action"
---

This is a post showing MDX in action. This starter now comes with MDX out-of-the-box!

```js
// you can write JSX in your Markdown!
<button>test</button>

これで記事が作成されていればOK。

(補足)frontmatterを読み込めるようにする

frontmatterとは、Markdownファイルの先頭に書く記事のタイトルとかの情報。

frontmatter

公式サイトの説明によると、 MDXだと以下のような感じでfrontmatterを読み込んだりができるらしい。

---
title: Building with Gatsby
author: Jay Gatsby
---

<h1>{props.pageContext.frontmatter.title}</h1>
<span>{props.pageContext.frontmatter.author}</span>

(Blog post content, components, etc.)

しかしこれが実際に試してみるとうまくいかない模様。

参考: GatsbyのMDXファイルでfrontmatterが参照できない問題

上記サイトに説明が書かれているため詳細は省くけど、どうやらsrc/pageにMDXファイルを置いていれば 前述のようなprops.pageContent.frontmatter.***の形式で読み込めるらしいが、 そうじゃないとうまくいかないらしい。

Gatsby Starter Blogではcontent/blogの中にMarkdownファイルを置いて記事として読ませているので、 読み込むことができない。

解決するためには、frontmatterをあらかじめ渡しておけばいい。 blog-post.js内にある、先ほど編集した記事を展開するコンポーネントMDXRendererのところに プロパティとしてfrontmatterを明示的に示す。以下のような感じ。

src/templates/blog-post.js
- <MDXRenderer itemProp="articleBody">{post.body}</MDXRenderer>
+ <MDXRenderer frontmatter={post.frontmatter} itemProp="articleBody">{post.body}</MDXRenderer>

これで、こんな感じに書くとfrontmatterが使える。

---
title: テスト記事
date: 2021-06-10 23:55:09
description:
---

* <b>title: {props.frontmatter.title}</b>
* <i>date: {props.frontmatter.date}</i>

先人の知恵は偉大。


止まらない実行を目指すITエンジニア。

© 2020-2022, Ryz