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
2 changes: 1 addition & 1 deletion .changeset/server-output-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Add server-output support and the `@cloudflare/nimbus-docs/adapters` export.

Nimbus can now target on-request (server) output in addition to static. A new `@cloudflare/nimbus-docs/adapters` public export ships the adapter recipes plus the shared `astro.config` and `wrangler.jsonc` emitters, and two new CLI verbs opt an existing site in: `nimbus-docs add server-output --adapter <vercel|node|netlify|cloudflare>` (alias `nimbus-docs add adapter-<id>`). The installer rewrites `astro.config` at the `// nimbus:adapter` marker and, for Cloudflare, creates a server `wrangler.jsonc` or replaces an exact Nimbus static config. Custom and alternate Wrangler configs are preserved with manual adaptation instructions.
Nimbus can now target on-request (server) output in addition to static. A new `@cloudflare/nimbus-docs/adapters` public export ships the adapter recipes plus the shared `astro.config` and `wrangler.jsonc` emitters, and two new CLI verbs opt an existing site in: `nimbus-docs add server-output --adapter <vercel|node|netlify|cloudflare>` (alias `nimbus-docs add adapter-<id>`). The installer rewrites `astro.config` at the `// nimbus:adapter` marker and, for Cloudflare, creates a server `wrangler.jsonc` or replaces an exact Nimbus static config. Cloudflare installs add request rendering when the active Nimbus config has no explicit rendering policy; explicit or ambiguous policies are preserved and receive an agent-ready handoff. Adapter dependencies are saved at their exact resolved versions so subsequent runs accept the installed declaration. Custom and alternate Wrangler configs are preserved with manual adaptation instructions.

Withdraw the `gated` config option because it did not hold as a confidentiality boundary. Existing `gated` config now fails with a migration error; to keep a page out of the build, move the page out of a routed content collection.

Expand Down
10 changes: 10 additions & 0 deletions .changeset/workers-prose-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@cloudflare/nimbus-docs": minor
"@cloudflare/create-nimbus-docs": minor
---

Add Cloudflare request rendering for canonical content collections.

Nimbus now supports collection-level build and request rendering policies with validated defaults and per-collection overrides. Request-rendered prose and API routes use response-aware page helpers, prepared API models, request-safe partial headings, 404 responses, and build-derived syntax-highlighting assets without shipping source OpenAPI specs to Workers. Cloudflare server scaffolds enable request rendering by default, and generated pnpm configuration installs Satteri's WASI fallback alongside the current architecture.

Preserve sitemap, Pagefind, Markdown, and agent-index discovery for request-rendered routes. Pin the tested sitemap integration, clean up synthetic Pagefind staging files transactionally, and generate cross-collection Open Graph images in new starters.
78 changes: 78 additions & 0 deletions .github/workflows/workers-feasibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Workers rendering acceptance

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
changes:
runs-on: ubuntu-latest
outputs:
relevant: ${{ steps.filter.outputs.relevant }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- id: filter
name: Detect Workers rendering changes
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: |
relevant=false
if [[ -z "$BASE_SHA" || "$BASE_SHA" == "0000000000000000000000000000000000000000" ]]; then
relevant=true
else
changed_files="$RUNNER_TEMP/workers-rendering-changed-files.txt"
git diff --name-only "$BASE_SHA" "$GITHUB_SHA" > "$changed_files"
while IFS= read -r file; do
case "$file" in
packages/nimbus-docs/*|packages/nimbus-docs/**|packages/nimbus-starter-source/*|packages/nimbus-starter-source/**|packages/create-nimbus-docs/*|packages/create-nimbus-docs/**|apps/www/registry/features/*|apps/www/registry/features/**|scripts/workers-feasibility-check.mjs|scripts/fixtures/workers-feasibility/*|scripts/fixtures/workers-feasibility/**|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|.npmrc|tsconfig.base.json|.github/workflows/workers-feasibility.yml)
relevant=true
break
;;
esac
done < "$changed_files"
fi
echo "relevant=$relevant" >> "$GITHUB_OUTPUT"

acceptance:
needs: changes
if: needs.changes.outputs.relevant == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm workers-feasibility:check

required:
name: Workers rendering required
needs: [changes, acceptance]
if: always()
runs-on: ubuntu-latest
steps:
- name: Require acceptance when relevant
env:
CHANGES_RESULT: ${{ needs.changes.result }}
RELEVANT: ${{ needs.changes.outputs.relevant }}
RESULT: ${{ needs.acceptance.result }}
run: |
if [[ "$CHANGES_RESULT" != "success" ]]; then
exit 1
fi
if [[ "$RELEVANT" != "true" && "$RELEVANT" != "false" ]]; then
exit 1
fi
if [[ "$RELEVANT" == "true" && "$RESULT" != "success" ]]; then
exit 1
fi
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ Run these inside your project:

Static by default — `pnpm build` emits `dist/`, which you can host anywhere.

Choose server output during scaffolding, or add an adapter later, when the site needs on-demand routes. Docs pages remain prerendered.
Choose Cloudflare server output during scaffolding to render canonical content collection routes on request. Existing projects can wire the adapter, then hand the project-specific rendering edit to a coding agent:

```sh
pnpm exec nimbus-docs add adapter-cloudflare
# adapter-vercel, adapter-netlify, or adapter-node
pnpm exec nimbus-docs add adapter-cloudflare --print | claude
```

Cloudflare is the first-class target: the default scaffold ships a `wrangler.jsonc`.
Expand Down Expand Up @@ -82,7 +82,7 @@ Components and utilities copy in as editable files. Features hand off a recipe y

## Built on

[Astro 7](https://astro.build) · Sätteri (Rust-based markdown) · Tailwind v4 · optional React 19. Static by default, with opt-in server output for Cloudflare, Vercel, Netlify, and Node.
[Astro 7](https://astro.build) · Sätteri (Rust-based markdown) · Tailwind v4 · optional React 19. Static output deploys anywhere; request-rendered server output currently targets Cloudflare.

## Status

Expand Down
14 changes: 10 additions & 4 deletions apps/www/registry/features/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,10 @@ export async function GET({ props }: { props: SlugProps }) {
### 4e. Scaffold the HTML route

The route is thin: `getApiStaticPaths` enumerates one path per page, and
`getApiPage(Astro)` builds the model and projects the page props + nav in a
single call — you hand both to `ApiLayout` (installed in 4a). `ApiLayout` composes `ApiSidebar` (verb chips +
`getApiRoute(Astro)` reads the page props and shared navigation prepared by the
content loader, then marks the current navigation path active. It never reads
or parses the OpenAPI source at request time. Hand both results to `ApiLayout`
(installed in 4a). `ApiLayout` composes `ApiSidebar` (verb chips +
active-section pruning), `ApiFieldRow` (recursive fields with type links), and
`ApiCodeRail` (server-generated code samples with a language switcher + a
response-example status toggle), rendering any page
Expand All @@ -298,21 +300,25 @@ Write `src/pages/api/[...slug].astro`:
<!-- api-reference-fixture:src/pages/api/[...slug].astro -->
```astro
---
import { getApiPage, getApiStaticPaths } from "@cloudflare/nimbus-docs";
import { getApiRoute, getApiStaticPaths } from "@cloudflare/nimbus-docs/runtime";
import Header from "@/components/Header.astro";
import { ApiLayout } from "@/components/ui/api-layout";
import BaseLayout from "@/layouts/BaseLayout.astro";

export const prerender = true;
export const getStaticPaths = getApiStaticPaths("api");

const { page, nav, collection, version, coordinate } = await getApiPage(Astro);
const result = await getApiRoute(Astro);
if (result instanceof Response) return result;
const { page, nav, collection, version, coordinate } = result;
const socialImage = `/og${page.href.replace(/\/$/, "")}.png`;
---

<BaseLayout
title={`${page.title} · API`}
description={page.description}
markdownUrl={page.markdownHref}
socialImage={socialImage}
collection={collection}
apiVersion={version ?? undefined}
coordinate={coordinate}
Expand Down
6 changes: 4 additions & 2 deletions apps/www/registry/features/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,13 +716,15 @@ route in 5j entirely.
import { Icon } from "astro-icon/components";
import ChangelogLayout from "@/layouts/ChangelogLayout.astro";
import { Badge } from "@/components/ui/badge";
import { getCollectionStaticPaths, getCollectionPageProps, withBase } from "@cloudflare/nimbus-docs";
import { getCollectionStaticPaths, getCollectionPage, withBase } from "@cloudflare/nimbus-docs";
import { components } from "@/components";

export const prerender = true;
export const getStaticPaths = getCollectionStaticPaths("changelog");

const { entry, Content } = await getCollectionPageProps<"changelog">(Astro);
const page = await getCollectionPage<"changelog">(Astro);
if (page instanceof Response) return page;
const { entry, Content } = page;
const { title, description, date, tags } = entry.data;

const iso = date.toISOString().slice(0, 10);
Expand Down
13 changes: 8 additions & 5 deletions apps/www/registry/features/new-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ conventions:
name).
- `src/pages/[...slug].astro` — read it. The new route will mirror this
shape exactly except for the helper names (`getCollectionStaticPaths` /
`getCollectionPageProps` instead of the `Docs` variants).
`getCollectionPage` instead of the `Docs` variants).
- `src/pages/[...slug]/index.md.ts` — read it. The new `.md` alternate
will mirror it.
- `src/layouts/DocsLayout.astro` — confirm it exists. The new route uses
Expand Down Expand Up @@ -191,7 +191,7 @@ Write `src/pages/<prefix>/[...slug].astro`:
import DocsLayout from "../../layouts/DocsLayout.astro";
import {
getCollectionStaticPaths,
getCollectionPageProps,
getCollectionPage,
getSidebar,
getPrevNext,
getBreadcrumbs,
Expand All @@ -205,7 +205,9 @@ import { components } from "../../components";
export const prerender = true;
export const getStaticPaths = getCollectionStaticPaths("<collection>");

const { entry, Content, headings } = await getCollectionPageProps<"<collection>">(Astro);
const page = await getCollectionPage<"<collection>">(Astro);
if (page instanceof Response) return page;
const { entry, Content, headings } = page;

const currentSlug = Astro.url.pathname.replace(/\/$/, "") || "/";
// Pass collection so the sidebar/prev-next resolve against the current
Expand All @@ -218,7 +220,8 @@ const prevNext = await getPrevNext(currentSlug, {
});
const breadcrumbs = await getBreadcrumbs(currentSlug);
const editUrl = await getEditUrl(entry);
const lastUpdated = entry.data.lastUpdated ?? await getLastUpdated(entry);
const lastUpdated = entry.data.lastUpdated ??
await getLastUpdated(entry);
const toc = getTOC(headings, entry.data.tableOfContents);
const markdownPath = `/<prefix>/${entry.id}/index.md`;
const basedMarkdownPath = withBase(markdownPath, import.meta.env.BASE_URL);
Expand Down Expand Up @@ -414,7 +417,7 @@ Ask the user whether to replace, skip, or show a diff first. The
to a Nimbus site. Blogs, API references, changelogs, glossaries, versioned
docs siblings — all the same shape underneath.
- The framework helpers `getCollectionStaticPaths(collection)` and
`getCollectionPageProps<C>(astro)` are sibling functions to
`getCollectionPage<C>(astro)` are sibling functions to
`getDocsStaticPaths`/`getDocsPageProps`. Use the `Collection` variants in
scaffolded routes; the `Docs` variants stay for the primary route only.
- The URL convention is intentional: primary `docs` mounts at root, every
Expand Down
11 changes: 7 additions & 4 deletions apps/www/registry/features/new-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ siblings:

- `getCollectionStaticPaths("docs-<slug>")` — takes the collection
name as an argument
- `getCollectionPageProps<"docs-<slug>">(Astro)` — takes the
- `getCollectionPage<"docs-<slug>">(Astro)` — takes the
collection name as a TypeScript generic

The snippet below uses the correct helpers. Copy it verbatim and
Expand All @@ -352,7 +352,7 @@ name with the user's slug):
import DocsLayout from "../../layouts/DocsLayout.astro";
import {
getCollectionStaticPaths,
getCollectionPageProps,
getCollectionPage,
getSidebar,
getPrevNext,
getBreadcrumbs,
Expand All @@ -366,7 +366,9 @@ import { components } from "../../components";
export const prerender = true;
export const getStaticPaths = getCollectionStaticPaths("docs-<slug>");

const { entry, Content, headings } = await getCollectionPageProps<"docs-<slug>">(Astro);
const page = await getCollectionPage<"docs-<slug>">(Astro);
if (page instanceof Response) return page;
const { entry, Content, headings } = page;

const currentSlug = Astro.url.pathname.replace(/\/$/, "") || "/";
const sidebar = await getSidebar(currentSlug, { collection: entry.collection });
Expand All @@ -376,7 +378,8 @@ const prevNext = await getPrevNext(currentSlug, {
});
const breadcrumbs = await getBreadcrumbs(currentSlug);
const editUrl = await getEditUrl(entry);
const lastUpdated = entry.data.lastUpdated ?? await getLastUpdated(entry);
const lastUpdated = entry.data.lastUpdated ??
await getLastUpdated(entry);
const toc = getTOC(headings, entry.data.tableOfContents);
const markdownPath = `/<slug>/${entry.id}/index.md`;
const basedMarkdownPath = withBase(markdownPath, import.meta.env.BASE_URL);
Expand Down
11 changes: 9 additions & 2 deletions apps/www/src/components/ui/search/providers/pagefind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ interface PagefindApi {

let pagefind: PagefindApi | undefined;

function withBase(url: string): string {
if (!url.startsWith("/")) return url;
const base = `/${(import.meta.env.BASE_URL ?? "/").replace(/^\/+|\/+$/g, "")}`;
if (base === "/" || url === base || url.startsWith(`${base}/`)) return url;
return `${base}${url}`;
}

/**
* Default Pagefind filters applied to every search.
*
Expand Down Expand Up @@ -70,11 +77,11 @@ export const provider: SearchProvider = {
const results = await Promise.all(search.results.slice(0, 10).map((result) => result.data()));
return results.map((result): SearchResult => ({
title: result.meta?.title ?? "Untitled",
url: result.url,
url: withBase(result.url),
snippet: result.excerpt,
subResults: result.sub_results
?.filter((sub): sub is Required<PagefindSubResult> => Boolean(sub.title && sub.url))
.map((sub) => ({ title: sub.title, url: sub.url })),
.map((sub) => ({ title: sub.title, url: withBase(sub.url) })),
}));
},
};
25 changes: 24 additions & 1 deletion apps/www/src/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,33 @@ Use `--print` to force the markdown output, skipping detection.

### Server adapters

For `adapter-cloudflare`, `adapter-vercel`, `adapter-netlify`, and `adapter-node`, the CLI installs the Astro adapter and rewrites the marked `output` block in `astro.config`. For Cloudflare, it creates a server-compatible `wrangler.jsonc` when none exists or replaces an exact Nimbus static config. Custom JSONC and alternate JSON/TOML configs stay untouched, with the required server settings printed for manual adaptation.
For `adapter-cloudflare`, `adapter-vercel`, `adapter-netlify`, and `adapter-node`, the CLI installs the Astro adapter and rewrites the marked `output` block in `astro.config`. It refuses to replace a different adapter or a non-literal `output` value.

<PackageManagers pkg="@cloudflare/nimbus-docs" type="dlx" args="add adapter-cloudflare" />

`adapter-cloudflare` adds request rendering when the Nimbus config has no explicit rendering policy. Existing policies are preserved; imported or ambiguous configurations receive a coding-agent handoff instead of a speculative rewrite. The command creates a server-compatible `wrangler.jsonc` when none exists or replaces an unchanged Nimbus static config; custom JSONC and alternate JSON/TOML configs stay untouched.

After completing the adapter install, expect:

```ts
const nimbusConfig = defineNimbusConfig({
rendering: { default: "request" },
// ...
});

export default defineConfig({
output: "server",
adapter: cloudflare({ prerenderEnvironment: "node" }),
integrations: [nimbus(nimbusConfig)],
});
```

When the command runs inside a detected coding agent, it emits a versioned runbook so the agent can safely adapt project-owned or split configuration. From a regular shell, use `--print` to request that runbook explicitly:

<PackageManagers pkg="@cloudflare/nimbus-docs" type="dlx" args="add adapter-cloudflare --print | claude" />

Always run the project’s production build afterward. See [Rendering policy](/configuration#rendering-policy) for per-collection build/request overrides.

The equivalent long form is `nimbus-docs add server-output --adapter <cloudflare|vercel|netlify|node>`.

## `nimbus-docs init`
Expand Down
Loading
Loading