next/mdx vs mdx-remote vs bundle/mdx
前言
學習 Next.Js,最容易看到的範例就是做一個 BLOG 了!
為了簡化開發方式,Next.Js 本身提供了對 md 的支援
但又為了讓他可以使用 React 組件,採用 mdx 格式 ── 就是 md + 簡單的 js
若只是當練手使用 Next.Js,確實會覺得這樣的支援沒什麼用處,畢竟大部份資料多半來源於 DB
但若是只要做幾個特定頁面,而有大量的靜態資料時,Next.Js 這樣的搭配方式,可以很輕易的解決問題!
而正巧手邊有這樣的需求,籍由 MDX,很輕鬆地透過 SSGs(Static Site Generators)完成所有頁面
僅須針對少數頁面額外實作就好!
若正巧有類似需求,去爬文會發現主要有 3 種解決方案!
以下列舉各自差異供參考
@next/mdx
即 Next.Js 官方提供的解法
由於 Next.Js 是 file-base route,可以直接將 mdx 檔置於pages路徑下,搭配此套件後
即可自動將 mdx 渲染成正常的 html 頁面
也最為簡單,安裝完套件,在next.config.js配置完,就直接生效了
只需簡單寫一個layout補上 tag 要搭配的 style 就可以
比如每個 TAG 都改由 MUI Library 渲染,或是有自己寫的 css
且也可以直接增加 React 組件,達到靈活調整頁面的目的
美中不足的小缺點就是
預設不支援 parse frontmatter,須另安裝套件gray-matter
因實務上不可能完全無樣式,所以mdx-layout.tsx是必寫的
在這裡多寫個getStaticsProps,透過gray-matter簡單寫個小邏輯來parse就可以了
程式碼整體來說還算乾淨
mdx 頁面內容大約如下
# 大標題
內容
## 二標題
`code`
import MyOwnComponent from "../components/my-own-component";
<MyOwnComponent />
import MdxLayout from '../components/layout/mdx-layout';
export default ({ children }) => <MdxLayout type={'PWA'}>{children}</MdxLayout>
mdx-layout
import React from 'react';
import { MDXProvider } from '@mdx-js/react';
import { MDXComponents } from "mdx/types";
import { Divider, Typography } from "@mui/material";
const components: MDXComponents = {
code: (props) => <code style={{ backgroundColor: "#e1fde2" }} {...props} />,
hr: (props) => <Divider sx={{ mt: 4, mb: 4 }} />,
h1: (props) => (
<Typography variant="h1" component={"h1"} gutterBottom mt={2}>
{props.children}
</Typography>
),
h2: (props) => (
<Typography variant="h2" component={"h2"} gutterBottom mt={2}>
{props.children}
</Typography>
),
p: (props) => <Typography variant="body1">{props.children}</Typography>,
};
export default function MdxLayout({type, children}: MdxLayoutProps) {
return (
<MDXProvider components={components}>
{children}
</MDXProvider>
);
}
next-mdx-remote
從套件名可直接看出,遠端取得 md 或 mdx 檔
不論來源是 DB 亦或是像 HEXO 這種 BLOG 產生器,即專案自己的特定目錄(如_POSTS)都可以
一般為了搭配SSGs,即純靜態頁面,通常就直接是專案目錄了
主張的就是資料來源與 Next.Js 解耦,雖然大部份人應該都還是放在一起 XD
用法也跟差不多
都是需要寫getStaticProps(),而此套件直接就支援front-matter了,只要多個參數就可以 parse
bundle/mdx
跟next-mdx-remote相似,是封裝比較完整的framework,不受限於 Nex.Js,可以在任何框架搭配使用
基本上優缺點也跟next-mdx-remote一樣,亦可直接提供參數自動取得 front matter
以在 Next.Js 使用來說,next-mdx-remote 跟 bundle/mdx基本上可說是等價
front matter 會遇到的一個坑
當front matter 有使用日期時
會出現下列錯誤訊息
Reason:
object(“[object Date]”) cannot be serialized as JSON. Please only return JSON serializable data types.
須包覆一層JSON.parse(JSON.stringify())
如下範例
import { serialize } from 'next-mdx-remote/serialize';
export async function getStaticProps() {
const mdx = `---
author: John Doe
date: 2022-08-18
---
# Hello World
Here's a component used inside Markdown:
<Hello />`;
const mdxSource = await serialize(mdx, {
parseFrontmatter: true,
});
return {
props: {
source: JSON.parse(JSON.stringify(mdxSource)),
},
};
}
後記
3 個方式都快速試一遍後,仔細想想我的需求
解耦MD檔案與Next.Js固然好
若希望將md檔名與文章實際網址拆分
以 hexo blog 的作法,就是在 front matter 增加parmalink來取得內容
會發現還需要額外寫getStaticPaths()產路由邏輯且控制好產出檔案到指定目錄
寫路由簡單
但為了方便比對實體檔案與路由的路徑對照
勢必需要增加一份db.json(也是 hexo 的作法)做參照,以便可以快速比對
發覺繼續做下去,其實就是重新發明 HEXO 了!
再回過頭來看了一下我的靜態資料
其實就是現有 md 檔案副檔名改名成mdx、尾端加上export就可以了
export default ({ children }) => <MdxLayout>{children}</MdxLayout>
md檔案異動機率趨近於 0 ,直接把少數中文檔名改成對應url英文,放置於pages
剩下的再寫個小腳本快速跑一下就結束的事
最後就直接採用原生的@next/mdx作法
雖然讓md檔稍醜了些,但換來的好處是
我不必逐一將每一頁面轉成 React 組件,重新刻一輪 html tag
只要略寫一些MDXComponents代上我要的樣式就收工了!
若真的靜態內容要異動,md 帶來的可讀性,還是遠勝於 html 的!
參考資料
How to Set Up MDX in Next.js
Guide to Using Mdx-bundler With Next.js