,\n }\n\n export default MDXComponents\n ```\n\n 接下來可以在應用程式的最外層注入這些客製化元件:\n\n ```jsx title=\"personal-blog/pages/_app.js\"\n import { MDXProvider } from '@mdx-js/react'\n import MDXComponents from '../components/mdx/MDXComponents'\n\n const MyApp = ({ Component, pageProps }) => {\n return (\n {post.frontmatter.title}
\n
身為一位開發者,肯定對於 Markdown 不陌生,身為 React 的開發者,肯定也對 Component 不陌生,而 MDX 語法正是 Markdown 與 Component 兩者的結合,它允許我們在 Markdown 語法裡穿插 JSX 語法,這使得 Markdown 速記語法如虎添翼,讓文章可以變得更為豐富多彩,簡單、易用、高度彈性等眾多好處讓我毫無懸念地採用 MDX 作為撰寫文章的主要語法,例如我可以這樣子寫文章:
# Work Experience
<Inline justifyContent="space-between" alignItems="center" gap="s" wrap="wrap">## Senior Backend Engineer, [ĒSEN Inc.](https://www.linkedin.com/company/eseninc/)*2022.03 - 2023.01*</Inline>
- Kicked off both business-facing and customer-facing projects with microservice architecture and domaindriven design (DDD) pattern- Implemented a reservation system with optimistic lock strategy to prevent double booking- Implemented the design system Essence to modularize and accelerate frontend development
慣例上使用 Markdown 撰寫部落格文章時都會搭配 語法來補足細節設定,像是文章標題、摘要、發佈日期等,不是文章主體,卻又與文章相關的次要資訊。因此本站使用 MDX 時也應該支援 Frontmatter 語法,一般都是在檔案開頭使用 這個 fenced block 夾住 yaml 語法,然後才是 MDX 語法的內文,例如:
---title: 文章標題publishDate: '2023-03-02'keywords: - 關鍵字1 - 關鍵字2---
# Title
paragraph.
一個部落格除了文章以外,其實還會有站內的頁面,像是、、等,他們也屬於作者所撰寫的內容,因此我希望這些網站頁面與部落格文章在撰寫時的體驗是一致的,都應該使用 MDX 語法來完成,差別在於網站頁面應遵守 Next.js 的檔案結構,而部落格文章則可以儲存於 Source Code 之外,所以我的檔案編排會是如以下所示結構:
personal-blog/ - content/ <--- 部落格文章可以放在 pages 資料夾外 - posts/ - post1.mdx - post2.mdx - post3.mdx - ... - pages/ <--- 網站頁面遵循 Next.js 的路由規則 - index.mdx - about-site.mdx - about-author.mdx - 404.js - ... - ...
在 Next.js 官方文件 Using MDX with Next.jsopen_in_new 當中已經有詳盡說明如何在你的專案使用 MDX,基本上照著操作就能順利渲染 了,以下是我的設定方式:
安裝相依
yarn workspace @app/personal-blog add \ @mdx-js/loader \ @mdx-js/react \ @next/mdx \ remark-frontmatter \ remark-gfm
設定
import createMDXDecorator from '@next/mdx'import { getMdxOptions } from './utils/mdx.mjs'
const withMDX = createMDXDecorator({ extension: /\.mdx?$/, options: { ...getMdxOptions(), // If you use `MDXProvider`, uncomment the following line. providerImportSource: '@mdx-js/react', },})
const nextConfig = { transpilePackages: ['@module/utils', '@module/essence'], reactStrictMode: true, pageExtensions: ['js', 'jsx', 'md', 'mdx'], // ...}
export default withMDX(nextConfig)
此處把 MDX 外掛的設定獨立出來,方便管理及重用:
import remarkFrontmatter from 'remark-frontmatter'import remarkGfm from 'remark-gfm'
export const getMdxOptions = () => { return { remarkPlugins: [ remarkFrontmatter, remarkGfm, ], rehypePlugins: [], }}
使用客製化元件
如果想要客製化渲染 MDX 內容時使用的 Component,首先要準備一個客製元件對照表:
import Text from '@module/essence/components/Text' // 使用來自 Design System 的元件
const MDXComponents = { p: (props) => <Text {...props} />,}
export default MDXComponents
接下來可以在應用程式的最外層注入這些客製化元件:
import { MDXProvider } from '@mdx-js/react'import MDXComponents from '../components/mdx/MDXComponents'
const MyApp = ({ Component, pageProps }) => { return ( <MDXProvider components={MDXComponents}> <Component {...pageProps} /> </MDXProvider> )}
export default MyApp
如果你的部落格可能需要支援多種風格的客製化元件,也可以考慮把 放在 Layout Component:
import { MDXProvider } from '@mdx-js/react'import Container from '@module/essence/components/Container'import MDXComponents from '../mdx/MDXComponents'
export default ({ children, ...rest }) => { return ( <Container fluid {...rest}> <MDXProvider components={MDXComponents}>{children}</MDXProvider> </Container> )}
但是要記得在 MDX 頁面使用 來指定 Layout:
import SitePageLayout from '../components/layout/SitePageLayout'export default ({ children }) => <SitePageLayout>{children}</SitePageLayout>
# 標題一
內文。
接下來讓我們來看看如何渲染 Source Code 以外的 MDX 檔案。
路由
假設我們希望每篇文章的網址形式長成像是 這樣,那麼按照 Next.js 的標準使用方式,我們可以建立頁面 來處理動態的 :
import { getPosts } from '../../../utils/post' // 下文將會解釋
const BlogPostPage = () => { // ... return }
export async function getStaticPaths() { const posts = getPosts() return { paths: posts.map(post => ({ params: { postSlug: post.slug, }, })), fallback: false, // can also be true or 'blocking' }}
export default BlogPostPage
渲染 MDX 內容
這裡我使用了 這個 Library 來輔助我把 MDX 文字渲染成 React 元件,所以首先安裝這個相依:
yarn workspace @app/personal-blog add next-mdx-remote
接著修改 ,將 MDX 原始碼 之後傳遞給 即可渲染出內容了:
import Container from '@module/essence/components/Container'import { MDXRemote } from 'next-mdx-remote'import { serialize } from 'next-mdx-remote/serialize'import { getMdxOptions } from '../../../utils/mdx.mjs'import { getPostByPostSlug, getPosts } from '../../../utils/post' // 下文將會解釋
const BlogPostPage = ({ post, mdxSource }) => { return ( <Container fluid> <h1>{post.frontmatter.title}</h1> <MDXRemote {...mdxSource} components={MDXComponents} /> </Container> )}
export async function getStaticPaths() { // ...}
export const getStaticProps = async ({ params }) => { const { postSlug } = params const post = getPostByPostSlug(postSlug) const mdxSource = await serialize(post.content, { mdxOptions: { ...getMdxOptions(), format: 'mdx', }, }) return { props: { post, mdxSource, }, }}
export default BlogPostPage
使用 讀取 MDX 原始內容
在上方的程式碼中,我們使用到了兩個 utility functions 與 ,分別可以取得文章列表,以及根據 Slug 取得特定文章。這背後的實作其實只有單純地操作檔案系統,也因此我們的部落格文章儲存位置不必受限於 Next.js 的規範。
處理 MDX 內容時會需要同時解析 Frontmatter,可以使用 這個 Library:
yarn workspace @app/personal-blog add gray-matter
我的 utility function 實作約略如下:
import fs from 'fs'import matter from 'gray-matter'import path from 'path'
class BlogPost { constructor(absoluteMDXFilePath) { this.absoluteFilePath = absoluteMDXFilePath this.parsedPath = path.parse(this.absoluteFilePath) this.fileContent = fs.readFileSync(this.absoluteFilePath, 'utf-8') const parsedFileContent = matter(this.fileContent) this.frontMatter = parsedFileContent.data this.slug = this.frontMatter.index?.partialSlug || this.parsedPath.name.replace(/.mdx$/, '') this.content = parsedFileContent.content }
toSerializable() { let serialized = { slug: this.slug, frontMatter: this.frontMatter, content: this.content, } return serialized }}
export const getPosts = () => { const files = fs.readdirSync(path.join(process.cwd(), 'content/posts'), { withFileTypes: true, }) const posts = files.map((file) => { let blogPost if (file.isDirectory()) { blogPost = new BlogPost( path.join(process.cwd(), 'content/posts', file.name, 'index.mdx') ) } else { blogPost = new BlogPost( path.join(process.cwd(), 'content/posts', file.name) ) } return blogPost.toSerializable() }) return posts}
export const getPostByPostSlug = (postSlug) => { const posts = getPosts() const post = posts.find((post) => post.slug === postSlug) return post}
使用上需要特別注意,我們可能會使用 來傳遞 post 的 instance 給 React Component,但是只有相容 JSON 的資料格式可以被傳遞,因此 提供了 這個方法來處理 instance 的序列化。
文章內容管理
文章內容管理