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.
Client Rendering
serialize() returns a JSON-safe payload of the compiled page.
Return it from your loader, and render it on the client:
const serialized = (await page.data.load()).serialize();'use client';
import {
rendererFromSerialized,
type MarkdownRendererSerializedOptions,
} from '@fumadocs/satteri/local-md/client';
export function Content({ serialized }: { serialized: MarkdownRendererSerializedOptions }) {
return rendererFromSerialized(serialized).renderSync().body;
}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, the Vite plugin invalidates the source through Vite's file watcher. Add the plugin to your config and connect the source:
import { defineConfig } from 'vite';
import { localContentPlugin } from '@fumadocs/satteri/local-md/dev/vite';
export default defineConfig({
plugins: [localContentPlugin()],
});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
