Local Markdown (Sätteri)
Content source for local Markdown content, compiled with Sätteri.
Introduction
@fumadocs/satteri/local-md is a content source for local Markdown/MDX files, compiled with Sätteri instead of the unified/remark pipeline.
It has the same API as @fumadocs/local-md and is also bundleless, so it works at runtime with no type-gen or bundler step.
Use it when you want the Sätteri compiler, its plugin model, and the Fumadocs Sätteri preset.
Differences
- Compilation always produces JavaScript, so there is no virtual JavaScript engine option. Rendering uses the native engine.
- Plugins are Sätteri plugins, configured through
satteriOptions, not remark/rehype plugins.
Do not put untrusted Markdown/MDX files in dir, they can run as JavaScript or reference server-side files.
Setup
npm install @fumadocs/satteri shikishiki is installed because it has to be externalized by the bundler.
Create a localMd instance and connect it to Fumadocs:
import { dynamicLoader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/satteri/local-md';
const docs = localMd({
dir: 'content/docs',
});
const docsLoader = dynamicLoader(docs.dynamicSource(), {
baseUrl: '/docs',
});
export async function getSource() {
return docsLoader.get();
}Schema
Pass frontmatterSchema and metaSchema to customize the validation schemas:
import { localMd } from '@fumadocs/satteri/local-md';
import { pageSchema, metaSchema } from 'fumadocs-core/source/schema';
const docs = localMd({
dir: 'content/docs',
frontmatterSchema: pageSchema.extend({
// ...
}),
metaSchema: metaSchema.extend({
// ...
}),
});Compiler Options
satteriOptions is passed through the Fumadocs Sätteri preset, so plugins and their options are configured the same way as in Fumadocs MDX:
const docs = localMd({
dir: 'content/docs',
satteriOptions: {
rehypeCodeOptions: {
themes: { light: 'github-light', dark: 'github-dark' },
},
mdastPlugins: [myPlugin()],
},
});Pass a function to build the options lazily.
Usage
getSource() returns a content loader. Render pages the same way as @fumadocs/local-md, importing from @fumadocs/satteri/local-md instead.
Each page has load(), which compiles the page and returns a renderer:
const page = source.getPage(slugs);
const { body, toc } = await (await page.data.load()).render();The renderer also has structuredData and exports for values the document or its plugins exported.
Include
<include> and the ::include directive are enabled by default:
<include>../shared/note.md</include>Hot Reload
The source reads files at runtime, so nothing watches them by default.
During development, run the dev server of @fumadocs/local-md alongside your app:
{
"scripts": {
"dev": "local-md dev -- next dev"
}
}Connect the source to it:
import { watchWithDevServer } from '@fumadocs/satteri/local-md/dev/ws';
if (process.env.NODE_ENV === 'development') {
void watchWithDevServer(docs);
}This invalidates the source on the server, while Next.js keeps the rendered page in the browser. Render <DevClient /> in your root layout so the page refreshes too:
import { DevClient } from '@fumadocs/satteri/local-md/dev/react-client';
export function RootLayout({ children }) {
return (
<RootProvider>
<DevClient />
{children}
</RootProvider>
);
}Included files are watched too. To wire up your own watcher, watch docs.dir and the files passed to docs.onDependency(), then call docs.invalidateFile(path) when a file changes.
Disable Revalidation
Use staticSource() when you only need a one-time snapshot, so a normal loader() works:
import { loader } from 'fumadocs-core/source';
export const source = loader(await docs.staticSource(), {
baseUrl: '/docs',
});How is this guide?
Last updated on
