---
title: "Pages and routing"
description: "How the filesystem under src/content/docs becomes your routes and sidebar."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.klounge.kr/llms.txt
> Use this file to discover all available pages before exploring further.

# Pages and routing

import { FileTree } from "@/components/ui/file-tree";

A page is a `.md` or `.mdx` file under `src/content/docs/`. Its path on disk is its URL and its place in the [sidebar](/navigation/sidebar) — there is no separate routing or navigation config.

## File to URL

| File | URL |
|---|---|
| `src/content/docs/get-started.mdx` | `/get-started` |
| `src/content/docs/guides/styling.mdx` | `/guides/styling` |
| `src/content/docs/guides/index.mdx` | `/guides` |

Slugs are lowercased and folder `index` files collapse to the directory URL. A directory's `index.mdx` becomes the landing page for that section.

- src/content/docs/
  - introduction.mdx
  - guides/
    - index.mdx
    - styling.mdx

## Custom Astro routes

Files you add under `src/pages/` use Astro's native routing and rendering semantics. In a server-output project, a page or endpoint renders on request by default; set Astro's `prerender` export when you want to make the choice explicit:

```astro title="src/pages/status.astro"
---
export const prerender = false;
---

<h1>Service status</h1>
```

```ts title="src/pages/api/ping.ts"
export const prerender = false;

export function GET() {
  return new Response("pong");
}
```

Use `export const prerender = true` to emit a custom route as static output during the build. No Nimbus route registration or allowlist is required.

This boundary is not limited to `prerender`. Dynamic segments, endpoint methods, redirects and responses, and other native Astro route behavior remain controlled by the route itself. Nimbus only intervenes when a route claims the exact pattern of a configured canonical content route, package-injected infrastructure, an active feature, or a published content entry.

Nimbus's `rendering` configuration controls canonical content-collection routes, not other files under `src/pages/`. Files scaffolded for `llms.txt`, Markdown alternates, Open Graph images, and `robots.txt` belong to your project and may use Astro's native rendering behavior like any other route.

## Page modes

Every page renders inside `DocsLayout` by default — sidebar, table of contents, breadcrumbs, prev/next. Opt a page out of all chrome with `mode`:

```yaml
---
title: Welcome
mode: custom
---
```

`mode: custom` is for landing pages and bespoke layouts. Per-column toggles (`sidebar: false`, `tableOfContents: false`) turn off individual pieces without going fully custom — see [Layouts](/styling/layouts).

## Drafts

`draft: true` keeps a page out of production builds and the `llms.txt` indexes, while still rendering under `astro dev` (treated as `noindex`). Use it for work in progress.

```yaml
---
title: Experimental feature
draft: true
---
```

## Redirecting a sidebar entry

`external_link` rewrites where the sidebar links a page without changing where it builds — useful for pointing an entry at another section or an off-site URL. See [Redirects](/navigation/redirects).

## Other collections

`src/content/docs/` is the primary collection, mounted at the site root. Additional collections (a `blog`, an `api`, a versioned `docs-v2`) mount under their own URL namespace. Register them in `src/content.config.ts` with the factories from `nimbus-docs/content`.

Nimbus's `docsCollection()`, `partialsCollection()`, and `componentsCollection()` factories prepare content for links, alternate Markdown/MDX versions, `llms-full.txt`, and partial headings automatically. If you register a loader that stores Markdown bodies directly, wrap it with `withNimbusMarkdown()`:

```ts title="src/content.config.ts"
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { withNimbusMarkdown } from "@cloudflare/nimbus-docs/content";

export const collections = {
  blog: defineCollection({
loader: withNimbusMarkdown(
  glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
),
  }),
};
```

The wrapper preserves the loader's methods and lifecycle. Use it when a loader stores entries with a Markdown `body`. Every general-purpose indexed collection must provide prepared Markdown for alternate Markdown/MDX versions and `llms-full.txt`; arbitrary data-only collections are not supported. `apiCollection()` is the purpose-built exception because Nimbus supplies its renderer. If a collection is not prepared, the build names it and tells you to wrap its loader.

## Custom route helpers

Route matching uses logical, unbased paths; links sent to a browser use based paths. Nimbus exports helpers for both sides from `@cloudflare/nimbus-docs/runtime`:

```ts
import {
  entryRouteKey,
  stripBase,
  withBase,
} from "@cloudflare/nimbus-docs/runtime";

const route = entryRouteKey(entry.id);
const requestedRoute = stripBase(Astro.url.pathname, import.meta.env.BASE_URL);
const href = withBase(`/${route}`, import.meta.env.BASE_URL);
```

- `entryRouteKey()` preserves Astro entry IDs while collapsing a final `/index`.
- `stripBase()` removes the configured base before route matching.
- `withBase()` adds the configured base when producing a browser URL.

Nimbus rejects ambiguous or unsafe generated routes during the build. Common causes include both `foo` and `foo/index`, collisions with reserved `llms.txt` routes, encoded path separators, and encoded `.` or `..` segments. The build error names the conflicting entries.

## Page URLs

`getDocsPage()` and `getCollectionPage()` return each page's URLs next to `entry`, `Content`, and `headings`, so page routes don't build them by hand. `getIndexedEntries()` returns the same values on each `IndexedEntry`.

| Field | Example | Description |
| --- | --- | --- |
| `markdownUrl` | `/blog/welcome/index.md` | The page's clean Markdown version. `/index.md` for the site root. |
| `sourceUrl` | `/blog/welcome/index.mdx` | The page's authored source. `undefined` for entries without an authored body. |
| `ogImageUrl` | `/og/blog/welcome.png` | The page's generated OG card. `/og/index.png` for the site root, `/og/blog.png` for the root of a collection mounted at `/blog`. |

The URLs follow the entry ID. Astro's glob loader slugifies file paths, which drops dots: `src/content/docs/1.1.1.1/encryption.mdx` gets the ID `1111/encryption`. Dots are kept where Astro doesn't slugify: a version prefix (`/v1.2/guide/`), an API operation (`payment.succeeded`), or a `slug` frontmatter field, which Astro uses as the ID. To allow `slug`, add it with `schemaFields: { slug: z.string().optional() }`.

Each URL is site-relative, without Astro's deployment base; the starter's layouts add the base when they emit it. The starter's page route uses the URLs directly:

```astro title="src/pages/[...slug].astro (excerpt)"
---
const page = await getDocsPage(Astro);
if (page instanceof Response) return page;
const { entry, Content, headings, markdownUrl, ogImageUrl } = page;
const socialImage = entry.data.socialImage ?? ogImageUrl;
---
```

`getOgImagePages()` returns the `pages` map for astro-og-canvas's `OGImageRoute`: one entry per indexed page, keyed so the card is written at that page's `ogImageUrl`, with the page's `IndexedEntry` as the value. The starter's `src/pages/og/[...slug].ts` passes it through; see [Customize OG cards](/ai/override-markdown#customize-og-cards).

Source: https://docs.klounge.kr/writing/pages-and-routing/index.mdx
