Astro
Setup Fumadocs on Astro with React islands.
Getting Started
Before continuing, make sure to configure:
- Astro with
@astrojs/react. - Tailwind CSS 4.
Fumadocs UI is a React library. On Astro, render it inside a React island and pass the current route information from your .astro page.
Installation
npm i fumadocs-core fumadocs-ui @astrojs/react @astrojs/mdx @astrojs/markdown-remark react react-dom @orama/orama takumi-js sharpConfiguration
Configure Astro, MDX, React, and Tailwind CSS:
// @ts-check
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
import mdx from '@astrojs/mdx';
import { unified } from '@astrojs/markdown-remark';
import {
rehypeCode,
remarkCodeTab,
remarkHeading,
remarkNpm,
remarkStructure,
} from 'fumadocs-core/mdx-plugins';
const remarkPlugins = [
remarkHeading,
remarkCodeTab,
remarkNpm,
[remarkStructure, { exportAs: 'structuredData' }],
];
const rehypePlugins = [rehypeCode];
export default defineConfig({
markdown: {
processor: unified({
syntaxHighlight: false,
remarkPlugins,
rehypePlugins,
}),
},
integrations: [
react(),
mdx({
extendMarkdownConfig: true,
syntaxHighlight: false,
}),
],
vite: {
plugins: [tailwindcss()],
},
});
Add the following to your Tailwind CSS file:
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';Content Collections
Create content collections for docs and meta files:
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './content/docs' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
}),
});
const meta = defineCollection({
loader: glob({ pattern: '**/*.{json,yaml}', base: './content/docs' }),
schema: z.object({
title: z.string().optional(),
description: z.string().optional(),
pages: z.array(z.string()).optional(),
icon: z.string().optional(),
}),
});
export const collections = {
docs,
meta,
};
Create a Fumadocs source from Astro content collections:
import type { StaticSource } from 'fumadocs-core/source';
import { loader } from 'fumadocs-core/source';
import { type CollectionEntry, getCollection } from 'astro:content';
import * as path from 'node:path';
import { structure, type StructuredData } from 'fumadocs-core/mdx-plugins';
export const source = loader({
source: await createMySource(),
baseUrl: '/',
});
export function getStructuredData(entry: CollectionEntry<'docs'>): StructuredData {
return structure(entry.body);
}
export function getPageImage(slugs: string[]) {
const segments = [...slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
}
async function createMySource() {
const out: StaticSource<{
metaData: CollectionEntry<'meta'>['data'];
pageData: CollectionEntry<'docs'>['data'] & {
_raw: CollectionEntry<'docs'>;
};
}> = {
files: [],
};
for (const page of await getCollection('docs')) {
const virtualPath = path.relative('content/docs', page.filePath!);
out.files.push({
type: 'page',
path: virtualPath,
data: {
...page.data,
_raw: page,
},
});
}
for (const meta of await getCollection('meta')) {
const virtualPath = path.relative('content/docs', meta.filePath!);
out.files.push({
type: 'meta',
path: virtualPath,
data: meta.data,
});
}
return out;
}
Create Pages
Create the shared Astro document layout:
---
import { ClientRouter } from 'astro:transitions';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<ClientRouter />
</head>
<body
class="dark flex flex-col min-h-screen"
transition:persist
transition:animate="none"
transition:name="page-layout"
>
<slot />
</body>
</html>
Create the React island that renders Fumadocs UI:
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsPage, type DocsPageProps } from 'fumadocs-ui/layouts/docs/page';
import type { Root } from 'fumadocs-core/page-tree';
import type { ReactNode } from 'react';
import { navigate } from 'astro:transitions/client';
import { RootProvider } from 'fumadocs-ui/provider/astro';
import type { AstroProviderProps } from 'fumadocs-core/framework/astro';
import SearchDialog from './search';
export function Docs({
tree,
children,
pathname,
params,
page,
}: {
tree: Root;
children: ReactNode;
pathname: string;
params: AstroProviderProps['params'];
page?: DocsPageProps;
}) {
return (
<RootProvider
pathname={pathname}
params={params}
navigate={navigate}
theme={{ enabled: false }}
search={{ SearchDialog }}
>
<DocsLayout
tree={tree}
themeSwitch={{
enabled: false,
}}
nav={{
title: 'Fumadocs on Astro',
}}
>
<DocsPage {...page}>{children}</DocsPage>
</DocsLayout>
</RootProvider>
);
}
Create a search dialog:
'use client';
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { oramaStaticClient } from 'fumadocs-core/search/client/orama-static';
import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
const oramaLanguageByLocale: Record<string, string | undefined> = {
ar: 'arabic',
hy: 'armenian',
bg: 'bulgarian',
cs: 'czech',
da: 'danish',
nl: 'dutch',
en: 'english',
fi: 'finnish',
fr: 'french',
de: 'german',
el: 'greek',
hu: 'hungarian',
// Orama 3 names its Hindi stemmer `indian`.
hi: 'indian',
id: 'indonesian',
ga: 'irish',
it: 'italian',
lt: 'lithuanian',
ne: 'nepali',
no: 'norwegian',
pt: 'portuguese',
ro: 'romanian',
ru: 'russian',
sr: 'serbian',
sl: 'slovenian',
es: 'spanish',
sv: 'swedish',
ta: 'tamil',
tr: 'turkish',
uk: 'ukrainian',
sa: 'sanskrit',
};
function getOramaLanguage(locale?: string) {
const code = locale?.toLowerCase().split(/[-_]/)[0];
return code ? (oramaLanguageByLocale[code] ?? 'english') : 'english';
}
function initOrama(locale?: string) {
return create({
schema: { _: 'string' },
// https://docs.orama.com/docs/orama-js/supported-languages
language: getOramaLanguage(locale),
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
client: oramaStaticClient({
initOrama,
locale,
}),
});
return (
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
</SearchDialogContent>
</SearchDialog>
);
}
Create the docs route, search endpoint, and metadata image endpoint:
---
import { render, type CollectionEntry } from "astro:content";
import { Docs } from "@/components/docs";
import defaultMdxComponents from "fumadocs-ui/mdx";
import "@/styles/global.css";
import { getPageImage, source } from "@/lib/source";
import Layout from "@/components/layout.astro";
import type { TOCItemType } from "fumadocs-core/toc";
interface Props {
page: CollectionEntry<"docs">;
}
export async function getStaticPaths() {
return source.getPages().map((page) => ({
params: { slug: page.slugs.length > 0 ? page.slugs.join("/") : undefined },
props: { page: page.data._raw },
}));
}
const { page } = Astro.props;
const { Content, headings } = await render(page);
const slugs = typeof Astro.params.slug === "string" ? Astro.params.slug.split("/") : [];
const image = getPageImage(slugs).url;
const toc: TOCItemType[] = headings.map((heading) => ({
depth: heading.depth,
title: heading.text,
url: "#" + heading.slug,
}));
---
<Layout>
<title>{page.data.title}</title>
<meta name="title" content={page.data.title} />
<meta name="description" content={page.data.description} />
<meta property="og:image" content={image} />
<Docs
tree={source.getPageTree()}
pathname={Astro.url.pathname}
params={Astro.params}
page={{ toc }}
client:load
>
<h1 class="font-semibold text-3xl">{page.data.title}</h1>
<p class="text-lg text-fd-muted-foreground mb-8">{page.data.description}</p>
<div class="prose flex-1">
<Content components={{ ...defaultMdxComponents }} />
</div>
</Docs>
</Layout>
Done
You can start writing documents at content/docs:
---
title: Hello World
description: Your first document
---
I love Fumadocs.How is this guide?
