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 shape as @fumadocs/local-md and is also bundleless, so it works fully at runtime with no type-gen or bundler step.
Reach for it when you want Sätteri's compiler and its plugin model, and to reuse the Fumadocs Sätteri plugins and 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.
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/dynamic';
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
Read the source in your route or layout and render it with Fumadocs UI, exactly as with @fumadocs/local-md:
import { getSource } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
export default async function Layout({ children }: LayoutProps<'/docs'>) {
const docs = await getSource();
return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>;
}Each page exposes load(), which compiles it once and returns a renderer:
const page = source.getPage(params.slug);
const { body, toc } = await (await page.data.load()).render();The renderer also exposes structuredData for search indexing, and exports for anything the document or its plugins exported, such as frontmatter, toc and structuredData.
Client Rendering
serialize() returns a JSON-safe payload, so a server can hand a compiled page to the client:
const serialized = (await page.data.load()).serialize();'use client';
import { rendererFromSerialized } from '@fumadocs/satteri/local-md/client';
export function Content({ serialized }) {
return rendererFromSerialized(serialized).renderSync().body;
}Hot Reload
The source works at runtime, so nothing watches your files by default.
On Vite, use its own watcher:
import { localContentPlugin } from '@fumadocs/satteri/local-md/dev/vite';
export default defineConfig({
plugins: [localContentPlugin()],
});import { watchWithVite } from '@fumadocs/satteri/local-md/dev/vite';
if (import.meta.env.DEV) {
watchWithVite(docs);
}Otherwise run the dev server from @fumadocs/local-md alongside your app, and connect to it:
{
"scripts": {
"dev": "local-md dev -- next dev"
}
}import { watchWithDevServer } from '@fumadocs/satteri/local-md/dev/ws';
// change it if you use a different framework (e.g. import.meta.env.DEV)
if (process.env.NODE_ENV === 'development') {
void watchWithDevServer(docs);
}To wire up your own watcher instead, 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
