Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clean-routes-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudflare/nimbus-docs": patch
"@cloudflare/create-nimbus-docs": patch
---

Allow user-owned Astro pages and scaffolded Markdown and `llms.txt` endpoints to use native rendering semantics while retaining entrypoint-aware checks for active Nimbus contracts and composing with unrelated integration routes. These dynamic endpoints now resolve their payloads when rendered on request. Endpoint helpers now live at `@cloudflare/nimbus-docs/agent-endpoints`; the existing `@cloudflare/nimbus-docs/publication` entrypoint remains supported.
2 changes: 1 addition & 1 deletion apps/www/registry/feature-route-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test("collection recipes guard disabled table-of-contents configuration", async
}
});

test("changelog serves and links its expanded source artifact", async () => {
test("changelog serves and links its expanded source version", async () => {
const source = await feature("changelog");
assert.match(source, /surface: "source"/);
assert.match(source, /sourcePath[\s\S]*index\.mdx/);
Expand Down
112 changes: 76 additions & 36 deletions apps/www/registry/features/ai-native.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
{
"name": "ai-native",
"type": "registry:feature",
"title": "Publish Markdown",
"description": "Add per-page Markdown versions, llms.txt indexes, llms-full.txt, robots.txt, and an AgentDirective to a Nimbus docs site.",
"title": "Markdown and llms.txt endpoints",
"description": "Add alternate Markdown/MDX versions, llms.txt indexes, llms-full.txt, robots.txt, and an AgentDirective to a Nimbus docs site.",
"markers": ["src/pages/llms.txt.ts", "src/pages/llms-full.txt.ts", "src/pages/[...slug]/index.md.ts"]
}
---

# Publish Markdown
# Markdown and llms.txt endpoints

You are helping the user publish Markdown versions and `llms.txt` indexes from an existing Nimbus docs site. These files are deterministic build output on every deployment provider.
You are helping the user add alternate Markdown/MDX versions and `llms.txt` indexes to an existing Nimbus docs site. Their generated content is deterministic on every deployment provider.

Read this entire file before making changes. The target project should already depend on `nimbus-docs` and use the starter-style routes/layouts.

Expand All @@ -32,50 +32,70 @@ Then wire the layout/page props:
- `src/layouts/DocsLayout.astro` accepts `markdownUrl` and forwards it to `BaseLayout`.
- `src/pages/[...slug].astro` computes `markdownUrl` for docs entries and passes it to `DocsLayout`.

Do not add an `ai` config block. Do not add an MCP server. This feature is build-time/static only.
Do not add an `ai` config block or an MCP server. Nimbus prepares the endpoint payloads at build time, while each endpoint may be prerendered or rendered on request.

## Reference implementation

Keep all five Markdown routes prerendered and use the prepared helpers from `@cloudflare/nimbus-docs/build`.
Keep all five endpoints prerendered and use the route helpers from `@cloudflare/nimbus-docs/agent-endpoints`.

```ts title="src/pages/[...slug]/index.md.ts"
import {
getPreparedMarkdownArtifact,
getPreparedMarkdownStaticPaths,
type PreparedMarkdownReference,
} from "@cloudflare/nimbus-docs/build";
getMarkdownPayload,
getMarkdownStaticPaths,
type MarkdownEndpointReference,
} from "@cloudflare/nimbus-docs/agent-endpoints";

export const prerender = true;

interface SlugProps {
artifact: PreparedMarkdownReference;
reference: MarkdownEndpointReference;
}

export const getStaticPaths = () =>
getPreparedMarkdownStaticPaths({ collection: "docs", surface: "markdown" });
interface SlugContext {
params: { slug?: string };
props: Partial<SlugProps>;
request: Request;
}

export const getStaticPaths = async () =>
getMarkdownStaticPaths({
collection: "docs",
surface: "markdown",
});

export async function GET({ props }: { props: SlugProps }) {
const artifact = await getPreparedMarkdownArtifact(props.artifact);
return new Response(artifact.body, {
headers: { "Content-Type": artifact.mediaType },
export async function GET({ params, props, request }: SlugContext) {
const payload = await getMarkdownPayload({
collection: "docs",
surface: "markdown",
slug: params.slug,
reference: props.reference,
context: { request },
});
if (!payload) return new Response("Not found", { status: 404 });
return new Response(payload.body, {
headers: { "Content-Type": payload.mediaType },
});
}
```

Create `src/pages/[...slug]/index.mdx.ts` from the same code, changing `surface: "markdown"` to `surface: "source"`.

```ts title="src/pages/llms.txt.ts"
import { getPreparedLlmsArtifact } from "@cloudflare/nimbus-docs/build";
import { getLlmsPayload } from "@cloudflare/nimbus-docs/agent-endpoints";

export const prerender = true;

export async function GET() {
const artifact = await getPreparedLlmsArtifact({
scope: "site",
surface: "index",
});
return new Response(artifact.body, {
headers: { "Content-Type": artifact.mediaType },
export async function GET(context: { request: Request }) {
const payload = await getLlmsPayload(
{
scope: "site",
surface: "index",
},
context,
);
if (!payload) return new Response("Not found", { status: 404 });
return new Response(payload.body, {
headers: { "Content-Type": payload.mediaType },
});
}
```
Expand All @@ -84,23 +104,43 @@ Create `src/pages/llms-full.txt.ts` from the same code, changing `surface: "inde

```ts title="src/pages/[section]/llms.txt.ts"
import {
getPreparedLlmsArtifact,
getPreparedLlmsStaticPaths,
type PreparedLlmsReference,
} from "@cloudflare/nimbus-docs/build";
getLlmsPayload,
getLlmsStaticPaths,
type LlmsEndpointReference,
} from "@cloudflare/nimbus-docs/agent-endpoints";

export const prerender = true;

interface SectionProps {
artifact: PreparedLlmsReference;
reference: LlmsEndpointReference;
}

export const getStaticPaths = () => getPreparedLlmsStaticPaths();
interface SectionContext {
params: { section?: string };
props: Partial<SectionProps>;
request: Request;
}

export async function GET({ props }: { props: SectionProps }) {
const artifact = await getPreparedLlmsArtifact(props.artifact);
return new Response(artifact.body, {
headers: { "Content-Type": artifact.mediaType },
export const getStaticPaths = async () =>
getLlmsStaticPaths();

export async function GET({ params, props, request }: SectionContext) {
const reference =
props.reference ??
(params.section
? ({
scope: "section",
surface: "index",
section: params.section,
} satisfies LlmsEndpointReference)
: null);
if (!reference) return new Response("Not found", { status: 404 });
const payload = await getLlmsPayload(reference, {
request,
});
if (!payload) return new Response("Not found", { status: 404 });
return new Response(payload.body, {
headers: { "Content-Type": payload.mediaType },
});
}
```
Expand All @@ -116,7 +156,7 @@ Run the user's package manager build command (`pnpm build`, `npm run build`, etc
- `dist/robots.txt` exists and includes a `Sitemap:` line.
- `dist/<slug>/index.md` exists for docs entries.
- `dist/<slug>/index.mdx` exists for authored docs entries.
- Section indexes such as `dist/<section>/llms.txt` list their Markdown versions.
- Section indexes such as `dist/<section>/llms.txt` list their alternate Markdown versions.
- HTML pages include `<link rel="alternate" type="text/markdown" ...>` for docs entries.
- HTML pages include the hidden `[data-ai-agent-directive]` block for docs entries.

Expand Down
6 changes: 3 additions & 3 deletions apps/www/registry/features/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "api-reference",
"type": "registry:feature",
"title": "OpenAPI reference",
"description": "Mount an OpenAPI (Swagger) spec as a routed reference collection with generated pages, per-page Markdown versions, and llms.txt coverage from one spec file. For hand-authored API docs written as MDX, use `new-collection` instead.",
"description": "Mount an OpenAPI (Swagger) spec as a routed reference collection with generated pages, alternate Markdown versions, and llms.txt indexes from one spec file. For hand-authored API docs written as MDX, use `new-collection` instead.",
"markers": ["src/pages/api/[...slug].astro"]
}
---
Expand All @@ -12,8 +12,8 @@

You are helping the user mount an **OpenAPI (Swagger) spec** as a first-class
reference collection on a Nimbus docs site. One spec file in, and the user
gets: a routed page per operation/schema/tag under `/api`, a clean Markdown
version of every page, and automatic `llms.txt` and `llms-full.txt` coverage.
gets: a routed page per operation/schema/tag under `/api`, an alternate Markdown
version of every page, and automatic `llms.txt` indexes and `llms-full.txt`.

The render is Nimbus's own — the spec is parsed once per build and projected
into a stable view-model. There is no third-party reference renderer.
Expand Down
70 changes: 48 additions & 22 deletions apps/www/registry/features/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -954,10 +954,10 @@ export async function GET() {
import { entryRouteKey, withBase } from "@cloudflare/nimbus-docs";
import { getEntry } from "astro:content";
import {
getPreparedMarkdownArtifact,
getPreparedMarkdownStaticPaths,
type PreparedMarkdownReference,
} from "@cloudflare/nimbus-docs/build";
getMarkdownPayload,
getMarkdownStaticPaths,
type MarkdownEndpointReference,
} from "@cloudflare/nimbus-docs/agent-endpoints";
import { config } from "virtual:nimbus/config";

export const prerender = true;
Expand All @@ -967,16 +967,29 @@ const absoluteUrl = (path: string) =>
new URL(withBase(path, import.meta.env.BASE_URL), config.site).href;

interface SlugProps {
artifact: PreparedMarkdownReference;
reference: MarkdownEndpointReference;
}

export const getStaticPaths = () =>
getPreparedMarkdownStaticPaths({ collection: COLLECTION, surface: "markdown" })
interface SlugContext {
params: { slug?: string };
props: Partial<SlugProps>;
request: Request;
}

export const getStaticPaths = async () =>
getMarkdownStaticPaths({ collection: COLLECTION, surface: "markdown" })
.then((paths) => paths.filter((path) => path.params.slug !== undefined));

export async function GET({ props }: { props: SlugProps }) {
const artifact = await getPreparedMarkdownArtifact(props.artifact);
const entry = await getEntry(COLLECTION, props.artifact.id);
export async function GET({ params, props, request }: SlugContext) {
const payload = await getMarkdownPayload({
collection: COLLECTION,
surface: "markdown",
slug: params.slug,
reference: props.reference,
context: { request },
});
if (!payload) return new Response(null, { status: 404 });
const entry = await getEntry(COLLECTION, payload.id);
if (!entry) return new Response(null, { status: 404 });
const data = (entry.data ?? {}) as Record<string, unknown>;
const title = String(data.title);
Expand Down Expand Up @@ -1018,7 +1031,7 @@ export async function GET({ props }: { props: SlugProps }) {
"",
`# ${title}`,
"",
artifact.content,
payload.content,
"",
`Source: ${absoluteUrl(sourcePath)}`,
"",
Expand All @@ -1034,25 +1047,38 @@ export async function GET({ props }: { props: SlugProps }) {

```ts
import {
getPreparedMarkdownArtifact,
getPreparedMarkdownStaticPaths,
type PreparedMarkdownReference,
} from "@cloudflare/nimbus-docs/build";
getMarkdownPayload,
getMarkdownStaticPaths,
type MarkdownEndpointReference,
} from "@cloudflare/nimbus-docs/agent-endpoints";

export const prerender = true;

interface SlugProps {
artifact: PreparedMarkdownReference;
reference: MarkdownEndpointReference;
}

export const getStaticPaths = () =>
getPreparedMarkdownStaticPaths({ collection: "changelog", surface: "source" })
interface SlugContext {
params: { slug?: string };
props: Partial<SlugProps>;
request: Request;
}

export const getStaticPaths = async () =>
getMarkdownStaticPaths({ collection: "changelog", surface: "source" })
.then((paths) => paths.filter((path) => path.params.slug !== undefined));

export async function GET({ props }: { props: SlugProps }) {
const artifact = await getPreparedMarkdownArtifact(props.artifact);
return new Response(artifact.body, {
headers: { "Content-Type": artifact.mediaType },
export async function GET({ params, props, request }: SlugContext) {
const payload = await getMarkdownPayload({
collection: "changelog",
surface: "source",
slug: params.slug,
reference: props.reference,
context: { request },
});
if (!payload) return new Response("Not found", { status: 404 });
return new Response(payload.body, {
headers: { "Content-Type": payload.mediaType },
});
}
```
Expand Down
41 changes: 27 additions & 14 deletions apps/www/registry/features/new-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ plain doc tree.
**For an OpenAPI spec, this is also the wrong recipe.** This recipe makes a
tree of hand-authored MDX pages. If the user wants their API reference
*generated from an OpenAPI/Swagger document* — pages per operation and schema,
Markdown versions and `llms.txt` coverage — use `nimbus-docs add api-reference`. Use this
alternate Markdown versions and `llms.txt` indexes — use `nimbus-docs add api-reference`. Use this
recipe for `api` only when they're writing the API docs by hand.

**This recipe owns the whole setup of a non-version collection.** You
Expand Down Expand Up @@ -115,7 +115,7 @@ URL convention — a `docs-v1` collection always mounts at `/v1/`, never at
`/docs-v1/`.

For every other collection, the URL prefix must match the collection name.
Per-page Markdown versions and the collection's `llms.txt` index use that identity as their mount prefix.
Alternate Markdown versions and the collection's `llms.txt` index use that identity as their mount prefix.

### Q4. Add a starter entry?

Expand Down Expand Up @@ -288,33 +288,46 @@ Write `src/pages/<prefix>/[...slug]/index.md.ts`:
*/

import {
getPreparedMarkdownArtifact,
getPreparedMarkdownStaticPaths,
type PreparedMarkdownReference,
} from "@cloudflare/nimbus-docs/build";
getMarkdownPayload,
getMarkdownStaticPaths,
type MarkdownEndpointReference,
} from "@cloudflare/nimbus-docs/agent-endpoints";

export const prerender = true;

const COLLECTION = "<collection>";

interface SlugProps {
artifact: PreparedMarkdownReference;
reference: MarkdownEndpointReference;
}

export const getStaticPaths = () =>
getPreparedMarkdownStaticPaths({ collection: COLLECTION, surface: "markdown" });
interface SlugContext {
params: { slug?: string };
props: Partial<SlugProps>;
request: Request;
}

export const getStaticPaths = async () =>
getMarkdownStaticPaths({ collection: COLLECTION, surface: "markdown" });

export async function GET({ props }: { props: SlugProps }) {
const artifact = await getPreparedMarkdownArtifact(props.artifact);
return new Response(artifact.body, {
headers: { "Content-Type": artifact.mediaType },
export async function GET({ params, props, request }: SlugContext) {
const payload = await getMarkdownPayload({
collection: COLLECTION,
surface: "markdown",
slug: params.slug,
reference: props.reference,
context: { request },
});
if (!payload) return new Response("Not found", { status: 404 });
return new Response(payload.body, {
headers: { "Content-Type": payload.mediaType },
});
}
```

Substitute `<collection>` in the `COLLECTION` constant.

To serve the expanded source URL referenced by the prepared markdown,
To serve the expanded source URL referenced by the Markdown payload,
mirror this route at `src/pages/<prefix>/[...slug]/index.mdx.ts` with
`surface: "source"`.

Expand Down
Loading
Loading