Gatsby で MDX 記法の記事

Markdown による記事投稿

このサイトを始めるにあたり、記事は Markdown 記法で書きたかったので gatsby-plugin-mdx を導入した。Markdown 記法が使えればよかったのでチュートリアルにも出てきた gatsby-transformer-remark を使おうと考えていたが、MDX を使うと React コンポーネントを挿入できるということだったので優位性を感じ導入した。一部省略しているが下記のようなプラグイン設定を行っている。

gatsby-config.js
{
    plugins: [
        {
            resolve: `gatsby-plugin-mdx`,
            options: {
                defaultLayouts: {
                    posts: require.resolve("./src/layouts/post-layout.js"),
                },
                gatsbyRemarkPlugins: [
                    {
                    resolve: `gatsby-remark-images`,
                    options: {
                        maxWidth: 500,
                    },
                    },
                ],
            },
        },
    ]

gatsby-node.js の方も一部省略だが下記のようにして、記事ページに URL を持たせた。

gatsby-node.js
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `Mdx`) {
    const fileNode = getNode(node.parent)
    createNodeField({
      node,
      name: `modifiedTime`,
      value: fileNode.modifiedTime,
    })
    createNodeField({
      node,
      name: `birthTime`,
      value: fileNode.birthTime,
    })
    const slug = createFilePath({ node, getNode, basePath: `posts` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  await createPostPages(graphql, createPage)
}

async function createPostPages(graphql, createPage) {
  const result = await graphql(`
    {
      allMdx {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  result.data.allMdx.edges.forEach(( {node} ) => {
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/layouts/post-layout.js`),
      context: {
        slug: node.fields.slug,
      }
    })
  })
}

コードブロック

コードブロックには「言語ごとのハイライト・タイトル表示・行番号表示」が欲しかったので prism-react-renderer を導入し、更にコードブロック用のコンポーネントを作成した。

src/components/codeblock.js
import React from 'react'
import Highlight, {defaultProps} from 'prism-react-renderer'
import theme from "prism-react-renderer/themes/oceanicNext";
import * as styles from "./codeblock.module.css"


export default function CodeBlock({children, className}) {
  let [language, title] = (className || '').split(':');
  language = language.replace(/language-/, '')
  const CodeTitle = () => {
    if (title) {
      return (
        <div className={styles.codeTitle}>
          <span>{title}</span>
        </div>
      )
    }
    return (
      <span></span>
    )
  }

  return (
    <Highlight {...defaultProps} theme={theme} code={children} language={language}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => {
        tokens.pop()
        return (
          <div className={styles.codeBlockRoot}>
            <CodeTitle />
            <pre className={`${styles.codePre} ${className}`} style={style}>
              {tokens.map((line, i) => {
                const {
                    style: s,
                    className: c,
                } = getLineProps({ line, key: i })
                return (
                  <div key={i} style={s} className={`${styles.codeLine} ${c}`}>
                    <span className={styles.codeLineNumber}>{i + 1}</span>
                    <span className={styles.codeLineContent}>
                      {line.map((token, key) => (
                        <span key={key} {...getTokenProps({ token, key })} />
                      ))}
                    </span>
                  </div>
                )
              })}
            </pre>
          </div>
        )
      }}
    </Highlight>
  )
}

そしたら今度はそのコンポーネントを MDX ファイルの中で使えるようにレイアウト側を編集する。こうすることでようやく Markdown のコードブロック記法で、ハイライト・タイトル・行番号の三拍子揃った表示ができるようになった。

src/layouts/posts-layout.js
import React from "react"
import { Link, graphql } from "gatsby"

const components = {
    pre: props => <div {...props} />,
    code: CodeBlock,
    Link,
}

export default function PostLayout ({ data }) {
  return (
        <MDXProvider
            components={components}
        >
            <MDXRenderer>{data.mdx.body}</MDXRenderer>
        </MDXProvider>
  )
}

export const query = graphql`
  query($slug: String!) {
    mdx( fields: { slug: { eq: $slug } }) {
      body
    }
  }
`