---
title: Versioned docs
description: Configure multiple documentation versions with package-backed source helpers
type: guide
summary: Use createVersionedSources to configure stable, pre-release, or host-based documentation versions.
url: /docs/versioned-docs
source: apps/template/content/docs/versioned-docs.mdx
prerequisites:
  - /docs/configuration
related:
  - /docs/proxy
  - /docs/llms-txt
---

# Versioned docs

Versioned docs let you serve multiple documentation sets from one Geistdocs project. Use them for stable and pre-release docs, previous major versions, or host-based version switching.

  Help me add versioned docs to this Geistdocs site. Inspect `source.config.ts`, `lib/geistdocs/source.ts`, app route files, and the docs layout, then propose a versioned source setup using `createVersionedSources`.

## Configure source collections

Create one Fumadocs collection for each version in `source.config.ts`:

```ts title="source.config.ts"
import {
  defineGeistdocsSourceConfig,
  geistdocsFrontmatterSchema,
} from "@vercel/geistdocs/source-config";
import { defineDocs } from "fumadocs-mdx/config";

export const v4docs = defineDocs({
  dir: "content/docs/v4",
  docs: {
    schema: geistdocsFrontmatterSchema,
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

export const v5docs = defineDocs({
  dir: "content/docs/v5",
  docs: {
    schema: geistdocsFrontmatterSchema,
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

export default defineGeistdocsSourceConfig();
```

## Create versioned sources

Use `createVersionedSources` in `lib/geistdocs/source.ts`:

```ts title="lib/geistdocs/source.ts"
import { createVersionedSources } from "@vercel/geistdocs/source";
import { v4docs, v5docs } from "@/.source/server";
import { config } from "./config";

export const versions = createVersionedSources({
  config,
  current: "v4",
  versions: [
    {
      id: "v4",
      label: "v4",
      docs: v4docs,
      baseUrl: "/docs",
    },
    {
      id: "v5",
      label: "v5 pre-release",
      docs: v5docs,
      baseUrl: "/docs",
      routePrefix: "/v5",
    },
  ],
});

export const geistdocsSource = versions.current;
export const source = geistdocsSource.source;
```

The returned object includes:

| Property | Type | Description |
| --- | --- | --- |
| `current` | `GeistdocsSourceBundle` | The source matching the `current` version ID. |
| `byId` | `Record<string, GeistdocsSourceBundle>` | A map of version IDs to source bundles. |
| `all` | `Array` | Version metadata and source bundles for rendering UI. |
| `currentVersion` | `string` | The active version ID. |

## Render versioned pages

Use the current version for `/docs` and a specific version for version-prefixed routes:

```tsx title="app/[lang]/docs/[[...slug]]/page.tsx"
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import { config } from "@/lib/geistdocs/config";
import { versions } from "@/lib/geistdocs/source";

const docsPage = createDocsPage({
  config,
  source: versions.current,
});

export default docsPage.Page;
export const generateStaticParams = docsPage.generateStaticParams;
export const generateMetadata = docsPage.generateMetadata;
```

```tsx title="app/[lang]/v5/docs/[[...slug]]/page.tsx"
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import { config } from "@/lib/geistdocs/config";
import { versions } from "@/lib/geistdocs/source";

const docsPage = createDocsPage({
  config,
  source: versions.byId.v5,
  getPageUrl: ({ page }) => `/v5${page.url}`,
  metadata: ({ metadata }) => ({
    ...metadata,
    robots: {
      index: false,
      follow: true,
    },
  }),
});

export default docsPage.Page;
export const generateStaticParams = docsPage.generateStaticParams;
export const generateMetadata = docsPage.generateMetadata;
```

Use `getPageUrl` when the route path differs from the source `baseUrl`. This keeps page actions and markdown links pointed at the public URL.

## Add a version selector

Use `GeistdocsVersionSelect` when readers need to move between versions:

```tsx title="components/geistdocs/docs-layout.tsx"
import { GeistdocsDocsLayout } from "@vercel/geistdocs/layout";
import { GeistdocsVersionSelect } from "@vercel/geistdocs/versions";
import { config } from "@/lib/geistdocs/config";

export const DocsLayout = ({ tree, children }: DocsLayoutProps) => (
  <GeistdocsDocsLayout
    config={config}
    sidebarTop={
      <GeistdocsVersionSelect
        current="v4"
        versions={[
          { id: "v4", label: "v4" },
          { id: "v5", label: "v5 pre-release", routePrefix: "/v5" },
        ]}
      />
    }
    tree={tree}
  >
    {children}
  </GeistdocsDocsLayout>
);
```

Each version can define an `icon` to override the default current/previous version icons:

```tsx
<GeistdocsVersionSelect
  current="v6"
  versions={[
    { id: "v6", label: "v6 latest", icon: <span>6</span> },
    { id: "v5", label: "v5", icon: <span>5</span>, routePrefix: "/v5" },
  ]}
/>
```

Use `GeistdocsRouteSelect` for framework, product, or route switchers that should not use version-specific labels:

```tsx
import { GeistdocsRouteSelect } from "@vercel/geistdocs/versions";

<GeistdocsRouteSelect
  ariaLabel="Select framework"
  current="nextjs"
  items={[
    {
      id: "nextjs",
      label: "Next.js",
      description: "React framework",
      icon: <span aria-hidden>N</span>,
      href: "/:path*",
    },
    {
      id: "sveltekit",
      label: "SvelteKit",
      description: "Svelte framework",
      icon: <span aria-hidden>S</span>,
      routePrefix: "/sveltekit",
      href: "/sveltekit/:path*",
    },
  ]}
/>
```

For host-based versions, use `href` instead of `routePrefix`:

```tsx
<GeistdocsVersionSelect
  current="v6"
  versions={[
    { id: "v6", label: "v6 latest" },
    { id: "v5", label: "v5", href: "https://v5.example.com/:path*" },
    { id: "v4", label: "v4", href: "https://v4.example.com/:path*" },
  ]}
/>
```

## Avoid 404s when switching versions

By default the selector swaps the route prefix without checking that the page
exists in the target version, so pages that exist in only one version 404 on
switch. Pass `paths` — computed server-side with `collectVersionSwitchPaths` —
and the selector falls back to the nearest existing ancestor (or the version's
docs root) instead:

```tsx title="app/[lang]/docs/layout.tsx"
import { collectVersionSwitchPaths } from "@vercel/geistdocs/source";
import { GeistdocsVersionSelect } from "@vercel/geistdocs/versions";
import * as root from "next/root-params";
import { config } from "@/lib/geistdocs/config";
import { versions } from "@/lib/geistdocs/source";

const DocsLayout = async ({ children }) => {
  const lang = await root.lang();

  return (
    <GeistdocsDocsLayout
      config={config}
      sidebarTop={
        <GeistdocsVersionSelect
          current={versions.currentVersion}
          paths={collectVersionSwitchPaths({ lang, versions })}
          versions={config.versions}
        />
      }
      tree={versions.current.source.pageTree[lang]}
    >
      {children}
    </GeistdocsDocsLayout>
  );
};
```

`collectVersionSwitchPaths` enumerates every page of every version from the
`createVersionedSources` output and returns paths relative to each version's
`routePrefix`, plus each version's docs root as the fallback landing page.

When an app rewrites loader URLs at render time (for example serving a
`baseUrl: "/docs"` source under `/v5` with a custom `getPageUrl`), pass the
same mapping so the collected paths match the public URLs. For sources that
span several loaders per version (for example separate docs and cookbook
sources), use the lower-level `collectVersionPaths` per version instead:

```tsx
import { collectVersionPaths } from "@vercel/geistdocs/source";

const paths = {
  v4: {
    fallbackPath: "/docs",
    paths: collectVersionPaths({ lang, sources: [v4Docs, v4Cookbook] }),
  },
  v5: {
    fallbackPath: "/docs",
    paths: collectVersionPaths({
      lang,
      routePrefix: "/v5",
      sources: [v5Docs, v5Cookbook],
    }),
  },
};
```

Fallback resolution only applies to `routePrefix` versions. Host-based
versions (`href`) always keep the plain path swap, since the target host's
pages can't be enumerated locally.

## Configure markdown routes

Each public route family needs a matching markdown route in `proxy.ts`. Read [Proxy and markdown routes](/docs/proxy) for the proxy configuration.

## Scope search to a version

The search dialog does not know which version a reader is on. Pass the current version to the provider as `search.options.tag` and filter on it in your search route, so a query answers from the version the reader is reading. Read [Search scope](/docs/provider#search-scope) for the setup.

## Next steps

- Read [Proxy and markdown routes](/docs/proxy) to connect versioned pages to `.md`, `.mdx`, and AI-agent rewrites.
- Read [llms.txt](/docs/llms-txt) to configure all-docs markdown output for multiple sources.
