MDX 支持
Velite 开箱即用地支持 MDX。您可以使用 MDX 编写内容,Velite 会自动为您渲染。
以下示例可能会对您有所帮助:
- examples/nextjs - Next.js 与 MDX 的示例项目。
- zce/taxonomy - shadcn-ui/taxonomy 的分支,使用了 Velite。
开始使用
例如,假设您有以下内容结构:
项目根目录
├── content
│ └── posts
│ └── hello-world.mdx
├── public
├── package.json
└── velite.config.js./content/posts/hello-world.mdx 是一个 MDX 文档,其内容如下:
---
title: Hello world
---
export const year = 2023
# 去年的降雪量
在 {year} 年,降雪量高于平均水平。
随后是温暖的春天,导致
许多附近河流出现洪水泛滥的情况。
<Chart year={year} color="#fcb32c" />使用 s.mdx() 模式可将编译后的 MDX 代码添加到您的内容集合中。
import { defineConfig, s } from 'velite'
export default defineConfig({
collections: {
posts: {
name: 'Post',
pattern: 'posts/*.mdx',
schema: s.object({
title: s.string(),
code: s.mdx()
})
}
}
})运行 velite build,您将获得以下数据结构:
{
"posts": [
{
"title": "Hello world",
"code": "const{Fragment:n,jsx:e,jsxs:t}=arguments[0],o=2023;function _createMdxContent(r){const a={h1:\"h1\",p:\"p\",...r.components},{Chart:c}=a;return c||function(n,e){throw new Error(\"Expected \"+(e?\"component\":\"object\")+\" `\"+n+\"` to be defined: you likely forgot to import, pass, or provide it.\")}(\"Chart\",!0),t(n,{children:[e(a.h1,{children:\"Last year’s snowfall\"}),\"\\n\",t(a.p,{children:[\"In \",o,\", the snowfall was above average.\\nIt was followed by a warm spring which caused\\nflood conditions in many of the nearby rivers.\"]}),\"\\n\",e(c,{year:o,color:\"#fcb32c\"})]})}return{year:o,default:function(n={}){const{wrapper:t}=n.components||{};return t?e(t,{...n,children:e(_createMdxContent,{...n})}):_createMdxContent(n)}};"
}
]
}默认情况下,Velite 会将 MDX 内容编译为函数体字符串,可在您的应用程序中用于渲染内容。
渲染 MDX 内容
首先,您可以创建一个通用组件来渲染编译后的 mdx 代码。它应接受该代码和一个在 MDX 内容中使用的组件列表。
./components/mdx-content.tsx:
import * as runtime from 'react/jsx-runtime'
const sharedComponents = {
// 在此处添加您的全局组件
}
// 将 Velite 生成的 MDX 代码解析为一个 React 组件函数
const useMDXComponent = (code: string) => {
const fn = new Function(code)
return fn({ ...runtime }).default
}
interface MDXProps {
code: string
components?: Record<string, React.ComponentType>
}
// MDXContent 组件
export const MDXContent = ({ code, components }: MDXProps) => {
const Component = useMDXComponent(code)
return <Component components={{ ...sharedComponents, ...components }} />
}然后,您可以使用 MDXContent 组件来渲染 MDX 内容:
./pages/posts/[slug].tsx:
import { posts } from '@/.velite'
import { Chart } from '@/components/chart' // 导入您的自定义组件
import { MDXContent } from '@/components/mdx-content'
export default function Post({ params: { slug } }) {
const post = posts.find(i => i.slug === slug)
return (
<article>
<h1>{post.title}</h1>
<MDXContent code={post.code} components={{ Chart }} />
</article>
)
}常见问题解答
如何在 MDX 中导入组件?
您不需要这样做,因为 Velite 的 s.mdx() 模式不会在构建时打包这些组件。无需构建导入树。这有助于减小您内容的输出体积。
例如,假设您为多个 MDX 文件提取了一个公共组件,并在这些 MDX 中导入了该组件。
export const Callout = ({ children }: { children: React.ReactNode }) => {
// 您的公共组件
return <div style={{ border: '1px solid #ddd', padding: '1rem' }}>{children}</div>
}---
title: Foo
---
import { Callout } from '../components/callout'
# Foo
<Callout>This is foo callout.</Callout>---
title: Bar
---
import { Callout } from '../components/callout'
# Bar
<Callout>This is bar callout.</Callout>::>
如果 Velite 使用捆绑器来编译您的 MDX,Callout 组件将被捆绑到每个 MDX 文件中,这会导致输出代码中存在大量冗余。
相反,只需在您的 MDX 文件中使用您想要的任何组件,而无需导入。
---
title: Foo
---
# Foo
<Callout>This is foo callout.</Callout>---
title: Bar
---
# Bar
<Callout>This is bar callout.</Callout>::>
然后,将组件注入到 MDXContent 组件中:
import { Callout } from '@/components/callout'
import { MDXContent } from '@/components/mdx-content'
export default function Post({ params: { slug } }) {
const post = posts.find(i => i.slug === slug)
return (
<article>
<h1>{post.title}</h1>
<MDXContent code={post.code} components={{ Callout }} />
</article>
)
}您还可以添加全局组件,以便它们对所有 MDX 文件可用。
import * as runtime from 'react/jsx-runtime'
import { Callout } from '@/components/callout'
const sharedComponents = {
// 在此处添加您的全局组件
Callout
}
const useMDXComponent = (code: string) => {
const fn = new Function(code)
return fn({ ...runtime }).default
}
interface MDXProps {
code: string
components?: Record<string, React.ComponentType>
}
export const MDXContent = ({ code, components }: MDXProps) => {
const Component = useMDXComponent(code)
return <Component components={{ ...sharedComponents, ...components }} />
}如果想捆绑 MDX 怎么办?
如果您可以接受输出体积的增加,那么捆绑 MDX 可能是获得更好可移植性的一个不错的选择。
您可以安装以下包来捆绑 MDX:
npm i esbuild @fal-works/esbuild-plugin-global-externals @mdx-js/esbuild --save-dev然后,为 MDX 捆绑创建一个自定义模式:
CAUTION
以下代码仅是一个简单的示例。您可能需要根据实际情况进行调整。
import { dirname, join } from 'node:path'
import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'
import mdxPlugin from '@mdx-js/esbuild'
import { build } from 'esbuild'
import type { Plugin } from 'esbuild'
const compileMdx = async (source: string, path: string, options: CompileOptions): Promise<string> => {
const virtualSourse: Plugin = {
name: 'virtual-source',
setup: build => {
build.onResolve({ filter: /^__faker_entry/ }, args => {
return {
path: join(args.resolveDir, args.path),
pluginData: { contents: source } // for mdxPlugin
}
})
}
}
const bundled = await build({
entryPoints: [`__faker_entry.mdx`],
absWorkingDir: dirname(path),
write: false,
bundle: true,
target: 'node18',
platform: 'neutral',
format: 'esm',
globalName: 'VELITE_MDX_COMPONENT',
treeShaking: true,
jsx: 'automatic',
minify: true,
plugins: [
virtualSourse,
mdxPlugin({}),
globalExternals({
react: {
varName: 'React',
type: 'cjs'
},
'react-dom': {
varName: 'ReactDOM',
type: 'cjs'
},
'react/jsx-runtime': {
varName: '_jsx_runtime',
type: 'cjs'
}
})
]
})
return bundled.outputFiles[0].text.replace('var VELITE_MDX_COMPONENT=', 'return ')
}
export const mdxBundle = (options: MdxOptions = {}) =>
custom<string>().transform<string>(async (value, { meta: { path, content, config }, addIssue }) => {
value = value ?? content
if (value == null) {
addIssue({ fatal: true, code: 'custom', message: 'The content is empty' })
return null as never
}
const enableGfm = options.gfm ?? config.mdx?.gfm ?? true
const enableMinify = options.minify ?? config.mdx?.minify ?? true
const removeComments = options.removeComments ?? config.mdx?.removeComments ?? true
const copyLinkedFiles = options.copyLinkedFiles ?? config.mdx?.copyLinkedFiles ?? true
const outputFormat = options.outputFormat ?? config.mdx?.outputFormat ?? 'function-body'
const remarkPlugins = [] as PluggableList
const rehypePlugins = [] as PluggableList
if (enableGfm) remarkPlugins.push(remarkGfm) // 支持 gfm (自动链接字面量、脚注、删除线、表格、任务列表)。
if (removeComments) remarkPlugins.push(remarkRemoveComments) // 移除 HTML 注释
if (copyLinkedFiles) remarkPlugins.push([remarkCopyLinkedFiles, config.output]) // 将链接的文件复制到公共路径,并将其 URL 替换为公共 URL
if (options.remarkPlugins != null) remarkPlugins.push(...options.remarkPlugins) // 应用 remark 插件
if (options.rehypePlugins != null) rehypePlugins.push(...options.rehypePlugins) // 应用 rehype 插件
if (config.mdx?.remarkPlugins != null) remarkPlugins.push(...config.mdx.remarkPlugins) // 应用全局 remark 插件
if (config.mdx?.rehypePlugins != null) rehypePlugins.push(...config.mdx.rehypePlugins) // 应用全局 rehype 插件
const compilerOptions = { ...config.mdx, ...options, outputFormat, remarkPlugins, rehypePlugins }
try {
return await compileMdx(value, path, compilerOptions)
} catch (err: any) {
addIssue({ fatal: true, code: 'custom', message: err.message })
return null as never
}
})然后,您可以在 velite.config.js 中使用自定义模式:
import { defineConfig, s } from 'velite'
import { mdxBundle } from './mdx'
export default defineConfig({
collections: {
posts: {
name: 'Post',
pattern: 'posts/*.mdx',
schema: s.object({
title: s.string(),
code: mdxBundle()
})
}
}
})