published: 2020/9/7 update: 2020/9/9
最近なんかmdx
流行ってますよね。markdownでjsx使えるのなかなかすごいと思うんですが、Blog書くときにこの機能いる?って感じがします。
記事書くだけならそんなに拡張性はいらないと思う。なので、今回は記事を書くときにmdx
を採用しなかった理由について書こうと思います。厳密にいうと、書くフォーマットはMarkdownを使って、レンダリングする際にmdx
に変換してしまいます。mdx
も使おうと思えば使えます。
個人的にブログ書くときにmdxを使う際に不安だったのが、
import snowfallData from './snowfall.json'
import BarChart from './charts/BarChart'
# Recent snowfall trends
2019 has been a particularly snowy year when compared to the last decade.
<BarChart data={snowfallData} />
こういうやつです...すごい便利だと思うんですが、テンプレートとしての統一がしにくくて、<BarChart>
とかをもし変更してしまうと割と容易にエラーが出たりしそうでちょっといやだなーという感じでした。
それともう一つの理由が、mdx
は、
export meta = {
title: "new Blog!"
}
みたいなことができるんですけど、dynamic import
するとうまく動かないんですよね。そもそもfontmatterでいいじゃんみたいな。
いろんなブログとか見たんですけど、exampleが下の感じで、
const meta = dynamic(() => import(`../_posts/${filename}`)).then((m) => m.meta);
見た目いけそうなんですけど、実際にはエラーが起こってうまく動かない。という感じで、ちょっとまだ記事に使うには不安が残るなーという感じを受けました。
それと記事書くときに不満なのが、markdownならVSCodeのExtensionとかが充実しているので、 生産効率が非常に高いんですけど、mdxはなんかこう、そのへんはまだまだかなーという気がします。なので、ブログ記事はできるだけ純粋なMarkdownで書けるようにしたいです。
ただ、next.jsのmdxシステムに関するシステムは結構すごくて、mdxをpages/posts/hoge.mdx
とかにおくととlocalhost:3000/posts/hoge
にそのままページとしてレンダリングできるんですよね。さらに、ピュアなMarkdownに対してもそれが適応できる。
その上、remark
とかrehype
系のプラグインをnext.config.js
に書くだけで全体に適用できるようになる、というの機能があり、すごく魅力的です。
以下のような感じです
// remark plugins
const remarkMath = require('remark-math')
const remarkFrontmatter = require('remark-frontmatter')
const remarkSlug = require("remark-slug");
const remarkHeadings = require('remark-autolink-headings')
const remarkFootnotes = require('remark-footnotes')
// rehype plugins
const rehypeKatex = require('rehype-katex')
const rehypePrism = require('@mapbox/rehype-prism')
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
remarkFrontmatter, remarkSlug, remarkHeadings, remarkFootnotes, remarkMath],
rehypePlugins: [rehypeKatex, rehypePrism],
}
})
module.exports = withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx']
})
pageExtensionsにmd
を追加することでmarkdownを直でレンダリングできるようになります。さらにコードシンタクスとかkatexとかに対応しています(参考)。AMPには対応してないようですが。
もし自前でやろうと思ったら多分getStaticProps
とかの中でファイル読み込んでremarkとrehypeでパースしてdangerousInnerHTML
とかで読み込むことになる気がしますし、それに比べると良い気がしました。
この便利さを考えると、mdx pluginを使ってmarkdown書くのが一番ラクだなあと思いました。
Markdownをレンダリングする方法を一番素直に考えると、以下のようなコードが想定されます。[postId].jsx
的な感じです。contentLoaderは名前で察してくれるとありがたいです。
import next/dynamic;
import Layout from "../components/Layout";
import {
getFilePath,
getFileMeta,
getMdNames,
} from "../lib/contentLoader"
const BlogPostPage = ({filename, meta}) => {
const MDContent = dynamic(() => import(`../post/${filename}`))
return (
<Layout meta={meta}>
<MDContent />
</Layout>
)
}
export async function getStaticProps({ params} ) {
const filename = params.postId + ".md"
const filepath = await getFilePath(filename);
const meta = await getFileMeta(filepath);
return {
props: {
filename: filename,
meta: meta,
}
}
}
export async function getStaticPaths() {
const mdNames = await getMdNames();
const paths = mdNames.map((mdName) => ({
params: {
postId: mdName
}
}));
return {
paths,
fallback: false,
}
}
やっていることは単純で、getStaticPaths
でfs
モジュールを使ってmarkdownファイルの一覧を取得した後、getStaticProps
でファイルの場所に戻して、ついでにメタデータをとってきて、そのパスに対応するmarkdownファイルと、Layoutにメタデータを渡します。
このときにLayoutもメタデータで指定したいなら、
layout:
path: /path/to/Layout.tsx
component: Layout
みたいなメタデータを作ってdynamic
を使えばそれも実現できます。
もはや大体これでいいじゃん、って思ったのですが、サイドバーが作れない。headerをうまくとってきてそれを元にサイドバーが作りたい。
そこで考えたのが、remarkのcustom loaderを作る方法です。
remark-mdx
を使うと、だいたい下のmdxファイルは以下のようにパースされます。
ただのfrontmatter付きのマークダウンファイルは以下のような感じでMDASTに変換されます。
---
title: A
date: a/a/a/a
layout:
path: ../../components/Layout
component: Layout
---
# a
## aa
### aaa
remark-frontmatter
を使うと、frontmatter部分はtype === yaml
のchildrenとして取得できるようになります。また、header部分はtype === headings
を探せば取得できます。つまり、MDASTをparseしてfrontmatter部分とheaderをmetadataとして取得することが可能です。
そして、layoutで指定されたコンポーネントを行頭でimportし、メタデータをexportし、importしたコンポーネントをexportします。
つまり、上のようなfrontmatter付きのMarkdownを
import Layout from "../../components/Layout"
export const meta = {
title: "A",
date: "a/a/a/a",
headers: [
{
value: "a",
depth: 1,
},
{
value: "aa",
depth: 2
},
{
value: "aaa",
depth: 3,
}
]
}
export default ({meta, children}) => <Layout meta={meta} children={children} />
# a
## aa
### aaa
のように変換してしまいます(実際はもう少し色々やりたいですね、headersを木にするくらいはしておきたい)。
こうすれば、markdownファイルを置くだけでカスタムコンポーネント付きのmdxに解釈されてレンダリングされるようになります。
さらにheaderの情報を含んだmetadataをコンポーネントが受け取れるので、sidebarやtocをJSX側で作ることができます。リファクタリングがしたくなれば、ほとんどカスタムローダー側を触れば解決しそうなところもいけてる気がします。あとこの方法のメリットは突如として
<button>Push!!!!!!!</button>
とか入れたくなったときに入れられることです。パースはmdx
に準拠してやってるので、突然mdxフォーマットで書いても自動で対応されます。
個人的にいい案だろって思ってるんですが、誰もこんなアプローチとってないので少し不安だったりします。なんか問題があるのだろうか(もっといい案が知りたい)。
記事に間違い等ありましたら、お気軽に以下までご連絡ください
E-mail: illumination.k.27|gmail.com ("|" replaced to "@")
Twitter: @illuminationK
Copyright © illumination-k 2021.