Gatsby製のマークダウンブログをmdxに置き換えた
- # ブログ
- # Gatsby
タイトルの通りなんですが、このブログのマークダウン展開を gatsby-transformer-remark から gatsby-plugin-mdx に置き換えたので、簡単に流れを共有します
なんで置き換えたの?
まず、Mdx を使うことで記事ファイル中に React のコンポーネントを埋め込めます
import { TagList } from "@components/molecules/tag-list.tsx";
<TagList tags={[`Gatsby`, `ブログ`]} isLink />
import { TagList } from “@mdx-components/tag-list”
<TagList tags={[Gatsby
, ブログ
]} isLink />
こんな感じです
あとは、マークダウン関連のカスタマイズをプラグインから剥がしたかったのが大きいです
例えば、
- 見出しに id を付与したいなーとか、
- 見出し一覧作りたいなーとか
- コードブロックにタイトルつけたいなーとか
ってときに、gatsby-transformer-remark
だと、プラグインを指して簡単に実装はできるんですけど、自分で納得の行くようにいじるみたいなのが面倒でした
gatsby-plugin-mdx
を使うと、この辺の自由度があがるので納得のいくように調整ができそうだなーと思いました
手順
公式で、mdx
に置き換えるためのガイドが公開されているので基本的にはこれに従います
How to convert an existing Gatsby blog to use MDX | Gatsby
公式のガイド通り gatsby-transformer-remark
と gatsby-plugin-feed
を置き換えます
$ yarn add gatsby-plugin-mdx gatsby-plugin-feed-mdx @mdx-js/mdx @mdx-js/react
$ yarn remove gatsby-transformer-remark gatsby-plugin-feed
{
- resolve: `gatsby-transformer-remark`,
+ resolve: `gatsby-plugin-mdx`,
options: {
+ extensions: [`.mdx`, `.md`],
- plugins: [
+ gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 590,
},
},
{
resolve: `gatsby-remark-responsive-iframe`,
options: {
wrapperStyle: `margin-bottom: 1.0725rem`,
},
},
`gatsby-remark-prismjs`,
`gatsby-remark-copy-linked-files`,
`gatsby-remark-smartypants`,
],
},
},
- `gatsby-plugin-feed`,
+ `gatsby-plugin-feed-mdx`
プラグインをそのまま gatsbyRemarkPlugins
に指してあげます
あとは、GraphQL のクエリでの記事データ取得を
allMarkdownRemark
=>allMdx
markdownRemark
=>mdx
html
=>body
- (使っていれば)
htmlAst
=>mdxAST
に置き換えてあげます
以下は僕の投稿用テンプレートの例です
// ...
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!) {
- markdownRemark(fields: { slug: { eq: $slug } }) {
+ mdx(fields: { slug: { eq: $slug } }) {
id
excerpt(pruneLength: 160)
- html
+ body
- htmlAst
+ mdxAST
fields {
slug
}
frontmatter {
title
date
description
category
tags
draft
thumbnail {
childImageSharp {
fluid(maxWidth: 590) {
aspectRatio
base64
sizes
src
srcSet
srcWebp
srcSetWebp
tracedSVG
}
}
}
}
}
}
`
あとは、記事データの展開部分を MDXRenderer
に置き換えてあげれば完了です
import { MDXRenderer } from "gatsby-plugin-mdx"
- <div dangerouslySetInnerHTML={{ __html: post.html }} />
+ <MDXRenderer>{post.body}</MDXRenderer>
htmlAst
と mdxAST
は形式が異なっていたので、使っていた場合は対応が必要になります
任意のタグを上書きする
以下のように、ファイルを設置します
import { wrapRootElement as wrap } from "path/to/mdx-root"
export const wrapRootElement = wrap
import React, { ReactNode } from "react"
import { MDXProvider, MDXProviderComponents } from "@mdx-js/react"
const components: MDXProviderComponents = {
// 置き換えに使うコンポーネントをここに書く
}
export const wrapRootElement = ({
element,
}: {
element: ReactNode
}): ReactNode => <MDXProvider components={components}>{element}</MDXProvider>
これで、components
に タグ => 置き換え先コンポーネントって感じで任意のタグをカスタマイズできます
例えば、見出し 2 の要素に id (ページ内リンク) を付与したい場合は、
// ...
const toValidSlug = (baseString: string): string => {
return baseString.replace(new RegExp(` `, `g`), `_`).toLowerCase()
}
interface H2Props {
children: string
}
const H2: React.FC<H2Props> = ({ children }: H2Props) => {
return <h2 id={toValidSlug(children)}>{children}</h2>
}
const components: MDXProviderComponents = {
// 置き換えに使うコンポーネントをここに書く
h2: H2
}
// ...
こんな感じで書くことが出来ます
これで、柔軟に要素をカスタマイズできるようになりました!
コードハイライト
Mdx への置き換えに伴って、gatsby-remark-prismjs の使用をやめました
代わりに prism-react-renderer を使ってます
元々、gatsby-remark-prismjs
には不満が結構大きくて、Gatsby で技術ブログを作る際の知見 # コードブロックのカスタマイズ | きむそん.dev
にも書いたんですけど、プラグインを指しつつ、スタイルを無理やり上書きしてカスタマイズするみたいな感じでした
あとデフォルトでコードブロックが親要素を飛び出してしまう問題があったりとか、結構面倒だった記憶があります
おかげでコードは汚いし、ちょっとしたことで問題が起きてました
今回置き換えをしようと思ったのも、コピーボタンに不具合が出たためでした
Approach
mdx
でコードブロックを書くと pre
タグの中に展開されるので、ここを prism-react-renderer
でハイライトしてあげます
import { MDXProvider, MDXProviderComponents } from "@mdx-js/react"
import Pre from "path/to/pre"
const components: MDXProviderComponents = {
// 置き換えに使うコンポーネントをここに書く
pre: Pre
}
// ...
置き換えに使う Pre
コンポーネントを準備します
Pre コンポーネント
props
から、コードブロックのメタ情報(コード情報、言語等)を取り出すためにパッケージを追加します
$ yarn add mdx-utils
mdx-utils の 型宣言の準備(TypeScript を使ってる場合)
mdx-utils には型ファイルがないみたいなので、最低限のものだけ準備しました
declare module "mdx-utils" {
interface ChildrenPropsBase {
mdxType: `code`
children: string
}
interface PreProps<ChildrenProps extends ChildrenPropsBase> {
children: {
props: ChildrenProps
}
}
function preToCodeBlock<ChildrenProps extends ChildrenPropsBase>(
preProps: PreProps<ChildrenProps>
): {
codeString: string
className: string
language: string
} & Omit<ChildrenProps, "className" | "children">
}
mdx-utils.preToCodeBlock
を使うことで、メタ情報を抜き出して、prism-react-render
を使ってハイライトします
$ yarn add prism-react-renderer
ハイライト部分はほぼ prism-react-renderer のサンプルそのままです
import React from "react"
import Highlight, { defaultProps, Language } from "prism-react-renderer"
import dracula from "prism-react-renderer/themes/dracula"
import { preToCodeBlock, ChildrenPropsBase, PreProps } from "mdx-utils"
interface CodeProps {
codeString: string
language: Language
}
const Code: React.FC<CodeProps> = ({
codeString,
language,
}: CodeProps) => {
// シンタクスハイライト用のコンポーネント
return (
<div>
<Highlight
{...defaultProps}
code={codeString}
language={language}
theme={dracula}
>
{({
className,
style,
tokens,
getLineProps,
getTokenProps,
}): React.ReactNode => (
<pre
className={className}
style={style}
>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })} key={i}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} key={key} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
)
}
type ChildrenProps = ChildrenPropsBase & {
className: string
}
const PreComponent: React.FC<PreProps<ChildrenProps>> = (
preProps: PreProps<ChildrenProps>
) => {
const props = preToCodeBlock<ChildrenProps>(preProps)
if (props) {
return <Code {...props} />
} else {
return <pre {...preProps} />
}
}
export default PreComponent
途中で使っている preToCodeBlock
の戻り値が
codeString
: コードブロックに書かれたコードlanguage
: コードブロックに付与した言語情報className
になっているので、これらをそのまま prism-react-renderer.Highlight
に渡してあげてる感じです
language
には、コードブロックに書いた言語部分がそのまま入るので、
js:hello.js
と書いてタイトルを抜き出すとかcodeString
からクリップボードへのコピーボタンを置くとか
結構自由にカスタマイズできます
終わりに
細々としたところで詰まって結構大変だった一応置き換えることができました
コードブロックの見た目が好みになってコピーボタンが治ったくらいしか変わってはないんですけど、だいぶコードがキレイになったんで満足してます
パフォーマンスだけちょっと気になるので、しばらく様子を見てみようと思います