Fumadocs Marble

Introduction

What is Fumadocs?

cover

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

On this page