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-remotebundle/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