# Fumadocs (Framework Mode): Astro
URL: /docs/manual-installation/astro
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/astro.mdx

Setup Fumadocs on Astro.

## Getting Started [#getting-started]

Before continuing, make sure to configure:

- Astro 7.
- Tailwind CSS 4.

Docs are MDX files loaded by Astro content collections. `@fumadocs/astro` adds the Markdown features of Fumadocs to them, and Fumadocs UI renders in React islands.

### Installation [#installation]

```npm
npm i fumadocs-core fumadocs-ui @fumadocs/astro @astrojs/react react react-dom @types/mdx
```

### Configuration [#configuration]

Add the Fumadocs integration, it runs on Sätteri, the default Markdown processor of Astro 7, and adds the React and MDX integrations:

```mjs title="astro.config.mjs"
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import fumadocs from '@fumadocs/astro';

export default defineConfig({
  integrations: [fumadocs({ components: './src/components/mdx.tsx' })],
  vite: {
    plugins: [tailwindcss()],
  },
});
```

See [Options](#options) for `components` and `includeProcessedMarkdown`.

The code in this guide imports `src/*` from `@/*`, add the alias to your `tsconfig.json`:

```json title="tsconfig.json"
{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"],
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

### Styles [#styles]

Add the following to your Tailwind CSS file:

```css title="src/styles/global.css"
@import 'tailwindcss';
/* [!code ++:2] */
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
```

### Content Collections [#content-collections]

Docs pages are MDX files, define their collections with the loaders of Fumadocs:

```ts title="src/content.config.ts"
import { defineCollection } from 'astro:content';
import { metaLoader, pageLoader } from '@fumadocs/astro/loaders';

export const collections = {
  docs: defineCollection({ loader: pageLoader() }),
  meta: defineCollection({ loader: metaLoader() }),
};
```

`pageLoader()` loads the MDX files in `content/docs`, `metaLoader()` loads its [meta files](/docs/page-conventions#meta). They apply `pageSchema` and `metaSchema` from `fumadocs-core/source/schema`. A `schema` on the collection replaces them, add fields with `pageSchema.extend({ ... })`.

### Source [#source]

Create a Fumadocs source from the collections:

```ts title="src/lib/source.ts"
import { getCollection } from 'astro:content';
import { loader } from 'fumadocs-core/source';
import { toFumadocsSource } from '@fumadocs/astro/source';

export const source = loader({
  source: toFumadocsSource(await getCollection('docs'), await getCollection('meta')),
  baseUrl: '/docs',
});
```

### MDX Components [#mdx-components]

Register the components of your MDX files:

```tsx title="src/components/mdx.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { DocsBody } from 'fumadocs-ui/layouts/docs/page';
import type { MDXComponents } from 'mdx/types';

export function getMDXComponents(components?: MDXComponents) {
  return {
    ...defaultMdxComponents,
    ...components,
  } satisfies MDXComponents;
}

// for the Fumadocs integration, `wrapper` gives the content its prose styles
export default getMDXComponents({ wrapper: DocsBody });
```

The integration renders them for docs pages, so don't pass them to `<Content />`. `wrapper` wraps the content of pages, `DocsBody` gives it the prose styles.

### Layout [#layout]

Fumadocs UI renders in React islands. Create a provider for them, with the route info of Astro:

```tsx title="src/components/provider.tsx"
import { navigate } from 'astro:transitions/client';
import type { ReactNode } from 'react';
import { RootProvider } from 'fumadocs-ui/provider/astro';
import SearchDialog from '@/components/search';

export interface ProviderProps {
  /** `Astro.url.pathname` */
  pathname: string;
  /** `Astro.params` */
  params: Record<string, string | undefined>;
  children?: ReactNode;
}

export function Provider({ pathname, params, children }: ProviderProps) {
  return (
    <RootProvider pathname={pathname} params={params} navigate={navigate} search={{ SearchDialog }}>
      {children}
    </RootProvider>
  );
}
```

`navigate` lets Fumadocs navigate with the client router of Astro, enable it with `<ClientRouter />` in your HTML layout (`transition:animate="none"` turns off its fade):

```astro title="src/layouts/Layout.astro"
---
import { ClientRouter } from 'astro:transitions';
import '@/styles/global.css';

interface Props {
  title: string;
  description?: string;
}

const { title, description } = Astro.props;
---

<html lang="en" transition:animate="none">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    {description && <meta name="description" content={description} />}
    <ClientRouter />
  </head>
  <body class="flex flex-col min-h-screen">
    <slot />
  </body>
</html>
```

### Docs Page [#docs-page]

Create the React island of docs pages, the content is passed as children:

```tsx tab="src/components/docs.tsx"
import { type SerializedPageTree, useFumadocsLoader } from 'fumadocs-core/source/client';
import type { TOCItemType } from 'fumadocs-core/toc';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { Provider, type ProviderProps } from '@/components/provider';
import { baseOptions } from '@/lib/layout.shared';

export interface DocsProps extends ProviderProps {
  tree: SerializedPageTree;
  title: string;
  description?: string;
  toc: TOCItemType[];
}

/**
 * The React island of docs pages, the content is passed as children.
 */
export function Docs({ tree, title, description, toc, children, ...props }: DocsProps) {
  const data = useFumadocsLoader({ tree });

  return (
    <Provider {...props}>
      <DocsLayout {...baseOptions()} tree={data.tree}>
        <DocsPage toc={toc}>
          <DocsTitle>{title}</DocsTitle>
          <DocsDescription>{description}</DocsDescription>
          {children}
        </DocsPage>
      </DocsLayout>
    </Provider>
  );
}
```
```tsx tab="src/lib/layout.shared.tsx"
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';

export function baseOptions(): BaseLayoutProps {
  return {
    nav: {
      title: 'Astro',
    },
  };
}
```

Create the route of docs pages:

```astro title="src/pages/docs/[...slug].astro"
---
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro';
import Layout from '@/layouts/Layout.astro';
import { Docs } from '@/components/docs';
import { source } from '@/lib/source';

export const prerender = true;

export const getStaticPaths = (() => {
  return source.getPages().map((page) => ({
    params: { slug: page.slugs.join('/') || undefined },
    props: { page },
  }));
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { page } = Astro.props;
const { body: Content, toc } = await page.data.load();
const pageTree = await source.serializePageTree(source.getPageTree());
---

<Layout title={page.data.title} description={page.data.description}>
  <Docs
    client:load
    transition:persist="docs"
    pathname={Astro.url.pathname}
    params={Astro.params}
    tree={pageTree}
    title={page.data.title}
    description={page.data.description}
    toc={toc}
  >
    <Content />
  </Docs>
</Layout>
```

With `transition:persist` on every page, Astro keeps the island and its state (e.g. sidebar, search) across navigations, Fumadocs moves the content of the new page into it. `prerender` keeps the pages static, also with `output: 'server'`.

### Search [#search]

Create the search dialog and the search endpoint:

```tsx tab="src/components/search.tsx"
import {
  SearchDialog,
  SearchDialogClose,
  SearchDialogContent,
  SearchDialogHeader,
  SearchDialogIcon,
  SearchDialogInput,
  SearchDialogList,
  SearchDialogOverlay,
  type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useStaticSearch } from 'fumadocs-core/search/client';

export default function DefaultSearchDialog(props: SharedProps) {
  const search = useStaticSearch();

  return (
    <SearchDialog {...search} {...props}>
      <SearchDialogOverlay />
      <SearchDialogContent>
        <SearchDialogHeader>
          <SearchDialogIcon />
          <SearchDialogInput />
          <SearchDialogClose />
        </SearchDialogHeader>
        <SearchDialogList />
      </SearchDialogContent>
    </SearchDialog>
  );
}
```
```ts tab="src/pages/api/search.ts"
import type { APIRoute } from 'astro';
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';

const server = createFromSource(source, {
  // https://docs.orama.com/docs/orama-js/supported-languages
  language: 'english',
});

export const GET: APIRoute = () => server.staticGET();
```

Astro builds a static site by default, the endpoint outputs the search indexes at build time and the dialog searches them in the browser. Learn more about [Document Search](/docs/headless/search).

### Done [#done]

You can start writing documents at `content/docs`:

```mdx title="content/docs/index.mdx"
---
title: Hello World
---

I love Fumadocs
```

See the [full example](https://github.com/fuma-nama/fumadocs/tree/dev/examples/astro) for the home and 404 pages, [LLM routes](/docs/integrations/llms) and [OG images](/docs/integrations/og/takumi).

## Rendering [#rendering]

With `components`, the MDX content of each page renders as one React island (`client:load`). Its components share a React tree and hydrate like on other frameworks: interactive code blocks, tabs, accordions and heading anchors work, and so do JSX props like `icon={<Icon />}`.

Parts that React can't render are rendered by Astro, inside the island:

- Highlighted code.
- Images, with the image pipeline of Astro.
- Components imported in the MDX file, including Astro components. Framework components keep the rules of Astro, add `client:load` to hydrate them.
- `{expressions}`, they can't use the components of your `components` module.

Props of the components in your `components` module are serialized into the island, they must be JSON-serializable (no functions).

The island is a React tree of its own, providers of your layout don't reach it. `wrapper` receives the props of `<Content />`, use it to add providers, like for [Internationalization](/docs/internationalization/astro). The theme state isn't shared either, style light/dark mode with CSS (e.g. `dark:` variants) rather than `useTheme()`.

Without `components`, the content renders as Astro MDX, without React. Pass components to `<Content />` like other Astro content:

```astro
<Content components={{ h2: Heading }} />
```

## Options [#options]

```js title="astro.config.mjs"
fumadocs({
  // module whose default export is the MDX components
  components: './src/components/mdx.tsx',
  // enable `getText('processed')` on pages
  includeProcessedMarkdown: false,
  // content directories, relative to the project root
  collections: {
    docs: { dir: 'content/docs' },
  },
});
```

The values above are the defaults, except `components`. Fumadocs only processes the MDX files of `collections`, other Markdown of your project is left to Astro.

The integration adds `@astrojs/react` and `@astrojs/mdx` with their default options, add them to `integrations` yourself to configure them.

### Collections [#collections]

Collections take the options above, the options of the integration are their defaults. For example, a blog with its own MDX components:

```js title="astro.config.mjs"
fumadocs({
  components: './src/components/mdx.tsx',
  collections: {
    docs: { dir: 'content/docs' },
    // [!code ++]
    blog: { dir: 'content/blog', components: './src/components/blog-mdx.tsx' },
  },
});
```

Load it with `pageLoader('blog')`, and create a source with `toFumadocsSource(await getCollection('blog'))`.

### Plugins [#plugins]

Add Sätteri plugins to docs with `mdastPlugins` and `hastPlugins`, as an array or a function receiving the default plugins. For example, to render `mermaid` code blocks with the [`Mermaid` component](/docs/markdown/mermaid) of your MDX components:

```npm
npm i @fumadocs/satteri
```

```js title="astro.config.mjs"
import { remarkMdxMermaid } from '@fumadocs/satteri/remark-mdx-mermaid';

fumadocs({
  mdastPlugins: [remarkMdxMermaid()],
});
```

They only run on docs, while plugins of the `satteri()` processor of Astro apply to every Markdown file.

The built-in plugins take the same options as [Fumadocs MDX](/docs/mdx/mdx#customize-built-in-plugins): `remarkHeadingOptions`, `remarkCodeTabOptions`, `remarkNpmOptions`, `remarkStructureOptions`, `rehypeCodeOptions` and `rehypeKatexOptions`.

Fumadocs highlights the code blocks of docs, configure it with `rehypeCodeOptions`. The `markdown.shikiConfig` of Astro only applies to other Markdown.

```js title="astro.config.mjs"
fumadocs({
  rehypeCodeOptions: {
    themes: { light: 'catppuccin-latte', dark: 'catppuccin-mocha' },
  },
});
```

## Page Data [#page-data]

Pages of `toFumadocsSource()` have:

```ts
// the MDX content, table of contents and structured data (for search)
const { body, toc, structuredData } = await page.data.load();

// structured data only
await page.data.structuredData();

// the source file, or the Markdown processed by Fumadocs (requires `includeProcessedMarkdown`)
await page.data.getText('raw');
await page.data.getText('processed');
```
