Local Markdown
Content source for local Markdown content.
Introduction
@fumadocs/local-md is a content source for local Markdown/MDX files. It is bundleless by design, so it works fully at runtime.
Compared to MDX Remote, it handles more of the pipeline (frontmatter, meta files, search indexing) while focused on local files.
Compared to Astro content collections, it compiles your content at runtime instead of through the bundler, so build-time image optimization is disabled.
Limitations
- No build-time image optimization.
- No imports/exports in MDX files, but you can pass components when rendering.
Setup
Install the package:
npm install @fumadocs/local-md 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/local-md';
const docs = localMd({
dir: 'content/docs',
// options
});
const docsLoader = dynamicLoader(docs.dynamicSource(), {
baseUrl: '/docs',
});
export async function getSource() {
return docsLoader.get();
}Schema
You may pass frontmatterSchema and metaSchema to customize the validation schemas:
import { localMd } from '@fumadocs/local-md';
import { pageSchema, metaSchema } from 'fumadocs-core/source/schema';
const docs = localMd({
dir: 'content/docs',
frontmatterSchema: pageSchema.extend({
// ...
}),
metaSchema: metaSchema.extend({
// ...
}),
});Usage
getSource() returns a normal content loader. Each page has page.data.load(), which compiles the page and returns a renderer.
Serialize it in the page, and render it in a React island:
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { Docs } from '@/components/docs';
import { getSource } from '@/lib/source';
export const prerender = true;
export const getStaticPaths = (async () => {
const source = await getSource();
return source.getPages().map((page) => ({
params: { slug: decodeURI(page.slugs.join('/')) || undefined },
props: { page },
}));
}) satisfies GetStaticPaths;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { page } = Astro.props;
const source = await getSource();
const pageTree = await source.serializePageTree(source.getPageTree());
const { serialize } = await page.data.load();
---
<Layout title={page.data.title}>
<Docs
client:load
pathname={Astro.url.pathname}
params={Astro.params}
tree={pageTree}
title={page.data.title}
render={serialize()}
/>
</Layout>Include
<include> is enabled by default:
<include>../shared/note.md</include>In Markdown files, use the ::include[../shared/note.md] directive with remark-directive configured:
import remarkDirective from 'remark-directive';
const docs = localMd({
dir: 'content/docs',
mdOptions: { remarkPlugins: [remarkDirective] },
});Hot Reload
@fumadocs/local-md reads files at runtime, so nothing watches them by default.
During development, the Vite plugin invalidates the source through Vite's file watcher, with no extra process or port. Add the plugin to your config and connect the source:
import { defineConfig } from 'astro/config';
import { localContentPlugin } from '@fumadocs/local-md/dev/vite';
export default defineConfig({
vite: {
plugins: [localContentPlugin()],
},
});The plugin adds your content directory to Vite's watcher, since content files sit outside the module graph. Because no module is invalidated, the plugin reloads the browser instead. Pass localContentPlugin({ reload: false }) to handle reloads yourself.
The source reloads when Markdown or MDX files change, including files they include. 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.
JavaScript Engine
MDX files (*.mdx) are executed with new Function, so they do not work where eval() is disallowed (for example, Cloudflare Workers).
Markdown files (*.md) render from the syntax tree without eval(). A built-in virtual JavaScript engine evaluates JSX generated by plugins. Replace it with rendererOptions.executor.
Do not put untrusted MDX files in dir, they run as JavaScript in your app.
Disable Revalidation
staticSource() returns a one-time snapshot with no revalidation. It works with loader() instead of dynamicLoader():
import { loader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/local-md';
const docs = localMd({
dir: 'content/docs',
});
export const source = loader(await docs.staticSource(), {
baseUrl: '/docs',
});How is this guide?
Last updated on
