Docs: Document new MDX APIs

This commit is contained in:
fuma-nama
2024-08-31 20:34:00 +08:00
parent 3c9e4916f2
commit ed83d01d8b
15 changed files with 447 additions and 253 deletions
+35
View File
@@ -0,0 +1,35 @@
---
'fumadocs-mdx': major
---
**Support declarative collections**
**why:** This allows Fumadocs MDX to be more flexible.
**migrate:**
You don't need `exports` anymore, properties are merged into one object by default.
```diff
- page.data.exports.toc
+ page.data.toc
- page.data.exports.default
+ page.data.body
```
A `source.config.ts` is now required.
```ts
import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
export const { docs, meta } = defineDocs();
export default defineConfig();
```
The `mdx-components.tsx` file is no longer used, pass MDX components to body instead.
Search indexes API is now replaced by Manifest API.
Please refer to the docs for further details.
@@ -54,7 +54,7 @@ sync(client, {
});
```
The `indexes` variable should be provided by your content source provider. If you are using Fumadocs MDX, see [Search Index Generation](/docs/mdx/search-index).
The `indexes` variable should be provided by your content source provider. If you are using Fumadocs MDX, see [Manifest](/docs/mdx/manifest).
Notably, it requires a `structured` property, this can be obtained from [Remark Structure](/docs/headless/mdx/structure), or sometimes supplied by the content source you are using.
+170 -50
View File
@@ -4,49 +4,164 @@ description: Learn to configure Fumadocs MDX
icon: Paperclip
---
## Plugin Options
## Configuration File
Fumadocs MDX offers webpack plugins and a Fumadocs [Source API](/docs/headless/source-api) adapter to integrate with Fumadocs.
You can configure Fumadocs MDX by passing options to `createMDX` in `next.config.mjs`.
Configure Fumadocs MDX by creating a `source.config.ts` file.
```ts
import createMDX from 'fumadocs-mdx/config';
### Global Options
const withMDX = createMDX({
// options
Shared options of Fumadocs MDX.
```ts title="source.config.ts"
import { defineConfig } from 'fumadocs-mdx/config';
export default defineConfig({
// global options
});
```
### Root Content Directory
```json doc-gen:typescript
{
"file": "./content/docs/mdx/props.ts",
"name": "GlobalConfig"
}
```
Fumadocs MDX only scans for a specific folder to produce the `.map` file, it's called the root content directory.
#### MDX Options
By default, it points to `./content` in the root directory. You can customise it with the `rootContentDir` option.
Customise the default MDX options of all MDX files.
### Map File Path
See [Default MDX Options](#default-mdx-options) for more details.
Customise the file path of `.map` file with the `rootMapPath` option.
#### Manifest
### Include
Enable the `generateManifest` option to generate a manifest file for production builds.
Include or filter files to the `.map` file, it uses micromatch under the hood which supports Regex.
See [Manifest](/docs/mdx/manifest) to learn more.
### Collections
Define a collection to parse a certain set of files.
```ts
import createMDX from 'fumadocs-mdx/config';
import { defineCollections } from 'fumadocs-mdx/config';
import { z } from 'zod';
const withMDX = createMDX({
include: ['./**/*.{md,mdx,json}'],
export const blog = defineCollections({
type: 'doc',
dir: './content/blog',
schema: z.object({
// allowed...
}),
});
```
### Current Working Directory
```json doc-gen:typescript
{
"file": "./content/docs/mdx/props.ts",
"name": "Collections"
}
```
Customise the current working directory with the `cwd` option, default to `process.cwd()`.
#### Schema
## MDX Options
The Zod schema to validate file data (frontmatter on `doc` type, content on `meta` type).
```ts
import { defineCollections } from 'fumadocs-mdx/config';
import { z } from 'zod';
export const blog = defineCollections({
type: 'doc',
dir: './content/blog',
schema: z.object({
name: z.string(),
}),
});
```
You can add additional properties to the output.
Note that the validation is done by build time, hence the output must be serializable.
You can also pass a function and receives the transform context.
```ts
import { defineCollections } from 'fumadocs-mdx/config';
import { z } from 'zod';
export const blog = defineCollections({
type: 'doc',
dir: './content/blog',
schema: (ctx) => {
return z.object({
name: z.string(),
testPath: z.string().default(ctx.path),
});
},
});
```
#### Type
The accepted type of collection.
| Type | Description |
| ------ | ---------------------- |
| `meta` | JSON/YAML File |
| `doc` | Markdown/MDX Documents |
#### MDX Options
You can also customise MDX options from collections.
Notice that passing `mdxOptions` to collection overrides all defaults from global config.
```ts
import { defineCollections, getDefaultMDXOptions } from 'fumadocs-mdx/config';
export const blog = defineCollections({
type: 'doc',
mdxOptions: getDefaultMDXOptions({
// mdx options
}),
});
```
We use `getDefaultMDXOptions` to apply default MDX options, it accepts the [Default MDX Options](#default-mdx-options).
For full control over MDX options, you can pass MDX options without `getDefaultMDXOptions`, which means no defaults will be applied (except the ones from MDX.js).
> This API only available on `doc` type.
#### Transform
A function to perform runtime transformation on collection entries.
See [Transform](/docs/mdx/transform).
### Define Docs
You can use `defineDocs` to define the required collections to work with Fumadocs.
It offers the same API as `defineCollections`.
```ts
import { defineDocs } from 'fumadocs-mdx/config';
export const { docs, meta } = defineDocs({
docs: {
// optional, you can pass options to each collection
},
});
```
## Default MDX Options
Fumadocs MDX uses the [MDX Compiler](https://mdxjs.com/packages/mdx) to compile MDX files into JavaScript files.
You can configure the compiler with the `mdxOptions` option.
Since some options should be applied by default, it exposes a `getDefaultMDXOptions` function that applies these defaults on the input MDX options.
To allow changing the default settings, it supports a different set of options.
> MDX options from global config are always applied automatically.
### Remark Plugins
@@ -55,31 +170,27 @@ These plugins are applied by default:
- [Remark Image](/docs/headless/mdx/remark-image) - Handle images
- [Remark Heading](/docs/headless/mdx/toc) - Extract table of contents
- [Remark Structure](/docs/headless/mdx/structure) - Generate search indexes
- Custom Remark Plugin - Exports the output generated by remark plugins above
- Remark Exports - Exports the output generated by remark plugins above
You can add other remark plugins with:
```ts
import createMDX from 'fumadocs-mdx/config';
import { getDefaultMDXOptions } from 'fumadocs-mdx/config';
import { myPlugin } from './remark-plugin';
const withMDX = createMDX({
mdxOptions: {
remarkPlugins: [myPlugin],
},
getDefaultMDXOptions({
remarkPlugins: [myPlugin],
});
```
You can also pass a function to control the order of remark plugins.
```ts
import createMDX from 'fumadocs-mdx/config';
import { getDefaultMDXOptions } from 'fumadocs-mdx/config';
import { myPlugin } from './remark-plugin';
const withMDX = createMDX({
mdxOptions: {
remarkPlugins: (v) => [myPlugin, ...v],
},
getDefaultMDXOptions({
remarkPlugins: (v) => [myPlugin, ...v],
});
```
@@ -92,26 +203,24 @@ These plugins are applied by default:
Same as remark plugins, you can pass an array or a function to add other rehype plugins.
```ts
import createMDX from 'fumadocs-mdx/config';
import { getDefaultMDXOptions } from 'fumadocs-mdx/config';
import { myPlugin } from './rehype-plugin';
const withMDX = createMDX({
mdxOptions: {
rehypePlugins: (v) => [myPlugin, ...v],
},
getDefaultMDXOptions({
rehypePlugins: (v) => [myPlugin, ...v],
});
```
### Rehype Code
### Customise Built-in Plugins
Customise the options for Rehype Code and [Shiki](https://shiki.style).
Customise the options of built-in plugins.
```ts
import createMDX from 'fumadocs-mdx/config';
import { getDefaultMDXOptions } from 'fumadocs-mdx/config';
const withMDX = createMDX({
mdxOptions: {
rehypeCode: { ... }
getDefaultMDXOptions({
rehypeCodeOptions: {
// options
},
});
```
@@ -124,10 +233,10 @@ Fumadocs MDX applies a remark plugin that turns `vfile.data` properties into exp
You can define additional properties to be exported.
```js
const withMDX = createMDX({
mdxOptions: {
valueToExport: ['dataName'],
},
import { getDefaultMDXOptions } from 'fumadocs-mdx/config';
getDefaultMDXOptions({
valueToExport: ['dataName'],
});
```
@@ -137,8 +246,19 @@ By default, it includes:
- `structuredData` for the Remark Structure Plugin
- `frontmatter` for the frontmatter of MDX (using `gray-matter`)
## Search Index Options
## Plugin Options
Pass the `buildSearchIndex` option to configure search index generation.
Fumadocs MDX offers loaders and a Fumadocs [Source API](/docs/headless/source-api) adapter to integrate with Fumadocs.
You can configure the plugin by passing options to `createMDX` in `next.config.mjs`.
See [Search Index Generation](/docs/mdx/search-index) to learn more.
### Config Path
Customise the path of config file.
```ts
import { createMDX } from 'fumadocs-mdx/next';
const withMDX = createMDX({
configPath: './my-config.ts',
});
```
+62 -36
View File
@@ -24,7 +24,7 @@ fumadocs-mdx @types/mdx
Add the plugin to your `next.config.mjs` file.
```js
import createMDX from 'fumadocs-mdx/config';
import { createMDX } from 'fumadocs-mdx/next';
const withMDX = createMDX();
@@ -36,8 +36,8 @@ const config = {
export default withMDX(config);
```
It generates a `.map.ts` file once you start the dev server or start building the app.
The map file contains all parsed files, you should add it to `.gitignore`.
It generates a `.source` folder once you start the dev server or start building the app.
The folder contains all parsed files, you should add it to `.gitignore`.
<Callout title="ESM Only" type='warn' className="mt-4">
@@ -45,20 +45,23 @@ The Next.js config must be a `.mjs` file since Fumadocs is ESM-only.
</Callout>
### Define MDX Components
To configure Fumadocs MDX, create a config file.
Create `mdx-components.tsx` in your root directory.
```ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';
```tsx
import type { MDXComponents } from 'mdx/types';
// Assume you're using Fumadocs UI
import defaultComponents from 'fumadocs-ui/mdx';
export const { docs, meta } = defineDocs();
```
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...defaultComponents,
...components,
};
### Post Install
You can add a post install script to generate types before executing CLI tools (e.g. ESLint).
```json title="package.json"
{
"scripts": {
"postinstall": "fumadocs-mdx"
}
}
```
@@ -67,14 +70,13 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
To integrate with Fumadocs, create:
```ts title="app/source.ts"
import { map } from '@/.map';
import { docs, meta } from '@/.source';
import { createMDXSource } from 'fumadocs-mdx';
import { loader } from 'fumadocs-core/source';
export const { getPage, getPages, pageTree } = loader({
baseUrl: '/docs',
rootDir: 'docs',
source: createMDXSource(map),
source: createMDXSource(docs, meta),
});
```
@@ -86,11 +88,50 @@ export const { getPage, getPages, pageTree } = loader({
next dev
```
A `.map.ts` file should be created. You can log and see if it is loaded correctly.
A `.source` folder should be created. You can log and see if it is loaded correctly.
See [Pages Conventions](/docs/ui/page-conventions)
to learn how to organize your pages.
### Usages
Usually, you'll interact with Fumadocs MDX through Source API (the `loader`).
You can see the type definitions to find the available properties.
To render the page, pass components to the body.
```tsx title="page.tsx (Fumadocs UI)"
import { getPage } from '@/app/source';
import {
DocsPage,
DocsBody,
DocsTitle,
DocsDescription,
} from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
import defaultComponents from 'fumadocs-ui/mdx';
export default async function Page({
params,
}: {
params: { slug?: string[] };
}) {
const page = getPage(params.slug);
if (!page) notFound();
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<page.data.body components={defaultComponents} />
</DocsBody>
</DocsPage>
);
}
```
## FAQ
### Built-in Properties
@@ -107,12 +148,12 @@ These properties are exported from MDX files by default.
You can customise the options passed to the MDX processor.
```js
import createMDX from 'fumadocs-mdx/config';
```ts title="source.config.ts"
import { defineConfig } from 'fumadocs-mdx/config';
import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math';
const withMDX = createMDX({
export default defineConfig({
mdxOptions: {
remarkPlugins: [remarkMath],
// When order matters
@@ -122,18 +163,3 @@ const withMDX = createMDX({
```
See [Configuration](/docs/mdx/configuration) to learn more.
### What is the `.map` file?
The `.map` file is generated by our Webpack plugin in build time.
It includes the information of files under your content directory,
which can be access by your Next.js app to implement SSG and many other features.
## Deep Dive
### How does it work?
Fumadocs MDX is a webpack plugin over Next.js to support reading/importing a MDX file by its name.
Because Next.js could only optimize static imports, it first generates a map file that imports **all supported files** under the root directory.
The imported files form an array called `map` in your `.map.ts` file, so that your Next.js app can access them without manually importing all of them.
+6 -8
View File
@@ -5,16 +5,14 @@ description: Output the last modified time of a document
## Usage
This feature is not enabled by default, you can enable this in `next.config.mjs`. Notice that it only supports Git as version control.
When using `git`, please ensure you have Git installed on your local machine and the repository is not shallow cloned as it relies on your local Git history.
This feature is not enabled by default, you can enable this from config file. Notice that it only supports Git as version control.
When using `git`, please ensure you have Git installed on your local machine and the repository is not shallow cloned, as it relies on your local Git history.
```js
import createMDX from 'fumadocs-mdx/config';
import { defineConfig } from 'fumadocs-mdx/config';
const withMDX = createMDX({
mdxOptions: {
lastModifiedTime: 'git', // [!code highlight]
},
export default defineConfig({
lastModifiedTime: 'git', // [!code highlight]
});
```
@@ -27,5 +25,5 @@ import { getPage } from '@/app/source';
const page = getPage('...');
console.log(new Date(page.data.exports.lastModified));
console.log(new Date(page.data.lastModified));
```
+88
View File
@@ -0,0 +1,88 @@
---
title: Manifest
description: Generate manifest for production builds
---
## Usage
Enable it from global config.
```ts
import { defineConfig } from 'fumadocs-mdx/config';
export default defineConfig({
generateManifest: true,
});
```
During a production build (`next build`), Fumadocs MDX will output a manifest file in `.source/manifest.json`.
You can use it to set up search indexes, like configuring Algolia Search.
### What is It?
The manifest file contains the compiled info of collection entries.
This includes their frontmatter, `structuredData` and `toc` (Table of Contents).
You can check the type definitions to see all available properties.
```ts
import type { Manifest } from 'fumadocs-mdx';
```
### Read Manifest
You can read the file after the build is finished.
```ts
import type { Manifest } from 'fumadocs-mdx';
import { readFileSync } from 'node:fs';
const manifest = JSON.parse(
readFileSync('.source/manifest.json').toString(),
) as Manifest;
```
### Example
To integrate with Algolia Search:
```ts title="update-indexes.ts"
import algosearch from 'algoliasearch';
import { sync } from 'fumadocs-core/search-algolia/server';
import type { Manifest } from 'fumadocs-mdx';
import { createGetUrl, getSlugs, parseFilePath } from 'fumadocs-core/source';
import path from 'node:path';
const getUrl = createGetUrl('/docs'); // we need to estimate the URL of pages
export async function updateSearchIndexes(manifest: Manifest): Promise<void> {
const client = algosearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
process.env.ALGOLIA_API_KEY,
);
await sync(client, {
document: process.env.NEXT_PUBLIC_ALGOLIA_INDEX ?? 'document',
documents: manifest.files
.filter((file) => file.collection === 'docs') // the collection name
.map((docs) => {
const url = getUrl(
getSlugs(parseFilePath(path.relative('content/docs', docs.path))),
);
if (!docs.data.structuredData)
throw new Error('`structuredData` is required');
return {
_id: docs.path,
title: docs.data.frontmatter.title as string,
description: docs.data.frontmatter.description as string,
url,
structured: docs.data.structuredData,
};
}),
});
}
```
> `frontmatter` is untyped. With type check enabled, you need to cast them to the correct type.
+2 -4
View File
@@ -8,9 +8,7 @@
"---Features---",
"last-modified",
"image",
"search-index",
"---Schema---",
"validation",
"multiple-types"
"manifest",
"transform"
]
}
@@ -1,26 +0,0 @@
---
title: Multiple Types
description: Multiple content types
---
You can have multiple document types, such as blog posts, or documents.
Use the `rootDir` option to restrict the folder it scans.
```ts
import { map } from '@/.map';
import { loader } from 'fumadocs-core/source';
import { createMDXSource } from 'fumadocs-mdx';
export const docs = loader({
baseUrl: '/docs',
rootDir: 'docs',
source: createMDXSource(map),
});
export const blogs = loader({
baseUrl: '/blog',
rootDir: 'blog',
source: createMDXSource(map),
});
```
+1
View File
@@ -0,0 +1 @@
export type { GlobalConfig, Collections } from 'fumadocs-mdx/config';
+69
View File
@@ -0,0 +1,69 @@
---
title: Transform
description: Runtime Transformation on collection entries
---
## Introduction
Fumadocs MDX bundles collection entries during build time, but this introduced some limitations on the configurations, forcing the output must be serializable.
You can use `transform` to transform collection entries on Next.js runtime.
It includes extra benefits:
- Unserializable output (e.g. functions)
- Server/Client Components
- Reference other parts of your Next.js apps
### How does it works?
This is possible by performing a transformation again on the bundled collection entries.
Everything is done on Next.js runtime, so using server/client components on `transform` is possible.
Be careful that:
- **Not cached by bundlers:** avoid expensive calculations in `transform`.
- **Use async imports:** Fumadocs MDX has to bundle the config file with esbuild before loading it. If importing a specific file results in errors, consider to use async import (`await import()`) instead.
### Usage
Pass a transform function.
```ts
import { defineCollections } from 'fumadocs-mdx/config';
export const blog = defineCollections({
type: 'doc',
transform: (entry) => {
return {
...entry,
test: 'hello world',
};
},
// other props
});
```
You can also use imports inside the function.
```ts
import React from 'react';
import { defineCollections } from 'fumadocs-mdx/config';
export const blog = defineCollections({
type: 'doc',
transform: async (entry) => {
const { Component } = await import('./my-server-component');
return {
...entry,
test: React.createElement(Component, {
// props
}),
};
},
// other props
});
```
### Build-time Transform
To transform collection entries during build time, use [Schema](/docs/mdx/configuration#schema) with [Zod `transform`](https://zod.dev/?id=transform) instead.
-56
View File
@@ -1,56 +0,0 @@
---
title: Validation
description: Validate your files via Zod
---
## Usage
The file validation is based on [Zod](https://zod.dev). When an invalid file is
detected, it will throw an error immediately.
To customise the schemas, you have to install `zod`.
```package-install
zod
```
Now you can override or extend the default zod schemas with the `schema`
option.
### Example
Extend the default frontmatter schema:
```ts title="app/source.ts"
import { createMDXSource, defaultSchemas } from 'fumadocs-mdx';
import { z } from 'zod';
import { loader } from 'fumadocs-core/source';
import { map } from '@/.map';
const frontmatterSchema = defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
});
export const utils = loader({
baseUrl: '/docs',
rootDir: 'docs',
source: createMDXSource(map, { schema: { frontmatter: frontmatterSchema } }),
});
```
### Typescript
Thanks to the Typescript magic, your custom Zod schema type should be automatically inferred.
```ts title="source.ts"
utils.getPages().data; // should be your own frontmatter
```
Infer the type of pages and meta files with build-in type helpers of Source API.
```ts
import type { InferMetaType, InferPageType } from 'fumadocs-core/source';
export type Page = InferPageType<typeof utils>;
export type Meta = InferMetaType<typeof utils>;
```
@@ -1,11 +1,13 @@
import { getPage, getPages } from '@/app/source';
import type { Metadata } from 'next';
import { DocsPage, DocsBody, DocsTitle } from 'fumadocs-ui/page';
import {
DocsPage,
DocsBody,
DocsTitle,
DocsDescription,
} from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
import defaultComponents from 'fumadocs-ui/mdx';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { Fragment } from 'react';
import { jsx, jsxs } from 'react/jsx-runtime';
export default async function Page({
params,
@@ -14,29 +16,13 @@ export default async function Page({
}) {
const page = getPage(params.slug);
if (!page) {
notFound();
}
const descriptionMd = toJsxRuntime(page.data.descriptionHast, {
development: false,
Fragment,
// @ts-expect-error -- jsx
jsx,
// @ts-expect-error -- jsx
jsxs,
// @ts-expect-error -- jsx
components: defaultComponents,
});
if (!page) notFound();
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<div className="mb-8 text-lg text-fd-muted-foreground">
{descriptionMd}
</div>
<page.data.body components={defaultComponents} />
</DocsBody>
</DocsPage>
+3 -48
View File
@@ -1,50 +1,5 @@
import {
defineDocs,
defineCollections,
frontmatterSchema,
defineConfig,
} from 'fumadocs-mdx/config';
import { z } from 'zod';
import { remark } from 'remark';
import {
rehypeCode,
remarkGfm,
remarkHeading,
remarkImage,
} from 'fumadocs-core/mdx-plugins';
import remarkRehype from 'remark-rehype';
import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
const processor = remark()
.use(remarkGfm)
.use(remarkImage, { useImport: false })
.use(remarkHeading)
.use(remarkRehype)
.use(rehypeCode);
export const { docs, meta } = defineDocs();
export const { docs, meta } = defineDocs({
docs: {
schema: frontmatterSchema.transform(async (v) => {
const parsed = processor.parse(v.description);
const hast = await processor.run(parsed);
return {
...v,
descriptionHast: hast,
test: 'goodbye hi',
};
}),
},
});
export const blog = defineCollections({
type: 'doc',
dir: './content/blog',
schema: z.object({
title: z.string(),
}),
});
export default defineConfig({
generateManifest: true,
mdxOptions: {},
});
export default defineConfig();
@@ -47,7 +47,7 @@ export async function generateStaticParams() {
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
const page = getPage(params.slug);
if (page == null) notFound();
if (!page) notFound();
return {
title: page.data.title,
+1 -1
View File
@@ -36,7 +36,7 @@ export interface Collections<
*/
files?: string[];
schema: Schema | ((ctx: TransformContext) => Schema);
schema?: Schema | ((ctx: TransformContext) => Schema);
/**
* content type