Introduction
What is Fumadocs?

Fumadocs has native support for CMS integrations, super simple and easy to use.
Quick Start
To use Marble CMS with Fumadocs, you can first copy the query.ts util from their Next.js starter.
Finally, use Fumadocs loader() API to integrate with your custom source.
import {
loader,
type MetaData,
type Source,
type VirtualFile,
} from 'fumadocs-core/source';
import { getPosts } from '@/lib/query';
import type { Post } from '@/lib/types';
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
const PageTag = 'page';
const RootCategory = 'docs';
export const source = loader({
baseUrl: '/docs',
source: await createMarbleSource(),
});
async function createMarbleSource(): Promise<
Source<{
metaData: MetaData;
pageData: Post;
}>
> {
return {
files: (await getPosts()).posts.flatMap((post) => {
if (!post.tags.some((tag) => tag.slug === PageTag)) return [];
const slugs = post.slug.split('/');
const isIndex = slugs.at(-1) === 'index';
const path: string[] = [];
if (post.category.slug !== RootCategory) {
path.push(post.category.slug);
}
path.push(isIndex ? 'index' : post.id);
return {
path: path.join('/'),
slugs: isIndex ? slugs.slice(0, -1) : slugs,
data: {
...post,
get structuredData() {
return getStructuredData(post);
},
},
type: 'page',
} satisfies VirtualFile;
}),
};
}
function getStructuredData(post: Post): StructuredData {
// simplified implementation, it's up to you
return {
headings: [],
contents: [
{
content: post.content,
heading: undefined,
},
],
};
}Page
To render your content, install dependencies html-react-parser , github-slugger.
And create the file: lib/render-html.tsx
import type { TableOfContents } from 'fumadocs-core/server';
import Slugger from 'github-slugger';
import parse, { type DOMNode, domToReact } from 'html-react-parser';
import { Heading } from 'fumadocs-ui/components/heading';
import { CodeBlock } from '@/components/code-block';
function renderFromHtml(content: string) {
const toc: TableOfContents = [];
const slugger = new Slugger();
const node = parse(content, {
replace(node) {
if (node.type !== 'tag') return node;
const heading = /^h(\d)$/.exec(node.name);
if (heading) {
const depth = Number(heading[1]);
const string = stringify(node);
const id = slugger.slug(string);
toc.push({
title: string,
depth,
url: `#${id}`,
});
return (
<Heading as={`h${depth}` as 'h1'} id={id}>
{domToReact(node.children as DOMNode[])}
</Heading>
);
}
if (node.name === 'pre') {
return <CodeBlock lang="ts" code={stringify(node)} />;
}
},
});
return { node, toc };
}
function stringify(node: DOMNode): string {
if (node.type === 'text') return node.data;
if ('children' in node) {
return node.children
.map((node) => {
if (node.type === 'cdata' || node.type === 'root') return;
return stringify(node);
})
.filter(Boolean)
.join('');
}
return '';
}Finally, in your page.tsx, render the page:
import { source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from 'fumadocs-ui/page';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { renderFromHtml } from '@/lib/render-html';
export const revalidate = 1000;
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const { node, toc } = renderFromHtml(page.data.content);
return (
<DocsPage toc={toc} lastUpdate={page.data.updatedAt}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
{page.data.coverImage && (
<img
alt="cover"
src={page.data.coverImage}
fetchPriority="high"
className="rounded-xl"
/>
)}
<DocsBody>{node}</DocsBody>
</DocsPage>
);
}Last updated on