Skip to content

Pages and routing

How the filesystem under src/content/docs becomes your routes and sidebar.

AI-generated · awaiting reviewUpdated View as Markdown

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 — 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:

src/pages/status.astroastro
---
export const prerender = false;
---

<h1>Service status</h1>
src/pages/api/ping.tsts
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:

---
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.

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.

---
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.

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():

src/content.config.tsts
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:

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:

src/pages/[...slug].astro (excerpt)astro
---
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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close