diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 95bceb977..81bec326c 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -14,6 +14,7 @@ "Cargo.toml", "packages/cloudflare-api-site/src/components/ui/**/*.ts", "packages/cloudflare-api-site/src/lib/cn.ts", - "packages/cloudflare-api-site/src/styles/globals.css" + "packages/cloudflare-api-site/src/styles/globals.css", + "packages/docs/src/content/**" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index 804de8e3d..e6d29b953 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,7 +12,12 @@ "ignorePatterns": ["node_modules", "dist", "**/_generated/**", ".wrangler"], "overrides": [ { - "files": ["packages/astro-fern/**", "packages/cloudflare-api-site/**", "packages/fern-forge/**"], + "files": [ + "packages/astro-fern/**", + "packages/cloudflare-api-site/**", + "packages/docs/**", + "packages/fern-forge/**" + ], "rules": { "curly": "off" } diff --git a/AGENTS.md b/AGENTS.md index 6aa5bdb9f..34cc05b40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,9 +7,11 @@ API documentation site for Forge OpenAPI specs, built on Astro. - **`packages/astro-fern`**: Generic Astro engine for rendering Fern-based API documentation. - **`packages/fern-forge`**: `astro-fern` extension that validates and adapts Forge OpenAPI metadata. - **`packages/docs-site`**: API documentation website consuming `astro-fern` and `fern-forge`. The OpenAPI spec is fetched from the `openapi.forge.json` GitHub release asset (or a local file via `FORGE_OPENAPI_SPEC`). +- **`packages/docs`**: The documentation site for Forge itself (the generator, its model and its plugins), built with Astro and Nimbus. `packages/docs/AGENTS.md` has its rules. ## Conventions +- **Docs move with the code**: Before finishing any change, check `packages/docs` for every page it affects and update them in the same change. That includes the CLI, the model and its Rust, TypeScript and WIT APIs, OpenAPI rules, extensions, error messages, the built-in targets, and `forge-plugin`. Examples and output on the pages are real, so rerun them rather than editing by hand. `packages/docs/AGENTS.md` has the rules for writing pages. - **Open Source Separation**: Generic packages (`astro-fern`, `fern-forge`) must remain free of Cloudflare-specific dependencies. - **Formatting & Linting**: `oxfmt` and `oxlint`, configured only at the root (`.oxfmtrc.json`, `.oxlintrc.json`). `pnpm check` verifies; `pnpm format` rewrites. - **Type Checking**: Strict TypeScript compiler settings across all packages via `pnpm typecheck`. @@ -30,6 +32,9 @@ pnpm typecheck # Run all unit tests pnpm test -# Build the docs site +# Build the API documentation site pnpm --filter docs-site build + +# Build the Forge docs site +pnpm --filter forge-docs build ``` diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore new file mode 100644 index 000000000..003892e1b --- /dev/null +++ b/packages/docs/.gitignore @@ -0,0 +1,17 @@ +# build output +dist/ +# generated types +.astro/ +# nimbus build scratch: materialized lint config and the route manifest. +# `nimbus.json` next to it is the opposite -- hand-owned, and committed. +.nimbus/ +# wrangler local state +.wrangler/ +# dependencies +node_modules/ +# environment variables +.env +.env.production +.dev.vars +# pagefind writes its index into dist/, but leaves a cache here +.pagefind/ diff --git a/packages/docs/AGENTS.md b/packages/docs/AGENTS.md new file mode 100644 index 000000000..4e95e6d5f --- /dev/null +++ b/packages/docs/AGENTS.md @@ -0,0 +1,122 @@ +# The Forge docs site + +Astro, with [Nimbus](https://nimbus-docs.com) (`@cloudflare/nimbus-docs`) as the docs framework. The +package handles content schemas, sidebar and TOC, MDX to markdown, search, OG cards, `llms.txt`, +build hooks, and the `nimbus-docs` CLI. Everything in `src/` is a real file in this repo and yours to +edit, including the files the scaffold wrote. + +`README.md` next to this file explains how the site looks and why. + +## Working in here + +This package is part of the repo pnpm workspace. Install once at the repo root: + +```sh +pnpm install +pnpm --filter forge-docs dev # http://localhost:4321 +pnpm --filter forge-docs build # static output in ./dist +pnpm --filter forge-docs check # astro check: types, content collections +pnpm --filter forge-docs lint:docs +``` + +`wrangler.jsonc` serves `dist/` as static assets on a Worker, with no script. It has no account or +route yet. + +## File layout + +```text +astro.config.ts # nimbus(defineNimbusConfig({...})): sidebar, lint rules, markdown plugins +nimbus.json # what the scaffold and the registry installed. Committed. +.nimbus/ # build scratch. Gitignored. +fonts/ # build-time only, for the OG cards. Not under public/ on purpose. +og-assets/ # the mark drawn on OG cards +src/ +├── components.ts # MDX globals registry: every component used in .mdx must be listed +├── components/ # ours: Hero, PipelineFigure, Features, NavList, Logo, Header, Prose +│ └── ui// # from the Nimbus registry +├── content/docs/**/*.mdx # the pages, one directory per sidebar group +├── content/partials/ # partials for ; none yet +├── content.config.ts +├── layouts/ # BaseLayout (head, theme bootstrap), DocsLayout (three columns) +├── lib/ # cn.ts +├── pages/ # [...slug].astro, 404, llms.txt, robots.txt, og/ +└── styles/ # globals.css (tokens and shell), prose.css +public/ # favicon, _headers +wrangler.jsonc +``` + +## Writing docs + +Frontmatter validates against Nimbus's `docsSchema`. `title` is required. Sidebar groups are declared +in `astro.config.ts`; position within a group comes from `sidebar.order`: + +```mdx +--- +title: My page +description: One-line summary. +sidebar: + order: 3 +--- + +Content here. The H1 comes from `title`; don't repeat it in the body. +``` + +Rules: + +- **Every claim is checked against the source.** The pages describe Forge's behaviour precisely: + which errors, which order, which defaults. Read the Rust doc comments (`src/lib.rs`, + `src/types.rs`, `src/method.rs`, `src/link.rs`) or run `forge` on a small document before writing + one. Output shown on a page is real output. +- **Every internal link must resolve.** `nimbus/internal-link` is an error, so a broken link fails + the build. It does not check `#anchors`; check those against the built HTML. +- **Quote a `description` that contains `: `.** YAML reads it as a mapping otherwise, and the build + fails. +- **Components must be PascalCase and registered in `src/components.ts`.** +- **Partials use ``.** Don't import `.mdx` directly. +- **A `mode: custom` page gets a bare `
`**, so its prose must be wrapped in ``. +- **Don't remove `` from `BaseLayout.astro`.** It points agents at `/llms.txt`. + +- **Other generators live under Compatibility Modes.** Forge's own conventions (methods named by a + dotted `operationId`, models by schema name, fields by wire name) are what every page describes. + Extensions that Forge reads for compatibility with another generator are described only on that + generator's page in `src/content/docs/compatibility/`, and no other page, component or file in + this package names that generator. Elsewhere, say "a compatibility mode" and point at the sidebar + group. Check with a case-insensitive grep for the generator's name before finishing. + +House style: no em dashes, and no `--` either, since it renders as two hyphens; every code fence +gets a language; no code block directly under a heading, so say what it is first. Plugins are +first-class: when a page describes a built-in target, say that it is a plugin like any other. + +## Public content + +This site is public. Never mention internal repositories, hosts, tickets or links in it. Describe +internal work in generic terms. + +## Adding things + +| Goal | Action | +| -------------------- | ------------------------------------------------------------------------------------------- | +| New doc page | `src/content/docs//.mdx`, with `sidebar.order`. The group autogenerates. | +| New sidebar group | A directory under `src/content/docs/` and an entry in `astro.config.ts`. | +| UI from the registry | `pnpm exec nimbus-docs add `, then register it in `src/components.ts` if MDX uses it. | +| OG card restyle | `src/pages/og/_og-card-config.ts`. | +| Check it builds | `pnpm exec nimbus-docs check --json`, then a build for link checking. | + +Two registry components are modified, so `add --overwrite` will undo the changes: +`page-actions/PageActions.astro` (a `data-pagefind-ignore` on the actions, a fixed locale and time +zone for the "Updated" date, and the divider at `/70`, which keeps it above 3:1 in both schemes) and +`breadcrumbs/Breadcrumbs.astro` (separators that inherit `text-muted-foreground`, for contrast). + +## Don't + +- Spend the orange accent on anything but calls to action and the active state. Prose links are + ink. +- Darken or theme-swap `--fg-orange`. It is Cloudflare orange, `#f6821f`, on both schemes. For + orange text on a light background, use `--fg-orange-text`, which is readable. +- Dim text with `opacity`, or Tailwind's alpha modifier. `--nb-muted-foreground` is the measured + secondary colour. The page-actions divider is the one measured exception. +- Recolour the logo. Its two oranges are the Cloudflare mark's own, in `Logo.astro`, the favicon and + `og-assets/`. +- Hand-add a component under `src/components/ui/` that the registry has. Use `nimbus-docs add`. +- Attach remark or rehype plugins via `mdx({ remarkPlugins })`: they are silently dropped. Markdown + transformations go in `markdown.mdastPlugins` and `hastPlugins`. diff --git a/packages/docs/README.md b/packages/docs/README.md new file mode 100644 index 000000000..965269f0f --- /dev/null +++ b/packages/docs/README.md @@ -0,0 +1,36 @@ +# Forge docs + +The documentation site for Forge, built with Astro and [Nimbus](https://nimbus-docs.com). + +```sh +pnpm install # at the repository root +pnpm --filter forge-docs dev # http://localhost:4321 +pnpm --filter forge-docs build # static output in ./dist +``` + +## How it looks, and why + +- **Cloudflare's palette, quietly.** Neutral greys carry the page. Cloudflare orange, `#f6821f` + (`--fg-orange`), is spent on calls to action and the active state, and nothing else, so it keeps + meaning "here". Orange text on light backgrounds uses `--fg-orange-text`, a darker orange, because + the brand orange on white is too faint to read as text. +- **Both schemes are first-class.** The site follows the system setting and the toggle. Every token + has a light and a dark value in `src/styles/globals.css`. +- **The logo is Cloudflare's mark**, in its own two oranges, in `src/components/Logo.astro`, the + favicon, and the OG card mark in `og-assets/`. +- **Inter and JetBrains Mono**, self-hosted through Fontsource, so there are no font requests to a + third party. +- **The landing figure is the argument.** `PipelineFigure.astro` shows the three stages (document, + model, plugins) because that is what Forge is. It is markup, not an image, so it is searchable, + selectable and themed. + +## Content + +Pages are in `src/content/docs/`, one directory per sidebar group: start, concepts, plugins, +targets, openapi, guides, reference. `AGENTS.md` has the rules for writing them; the most important +is that every statement about Forge is checked against its source or its output. + +## Deployment + +`wrangler.jsonc` serves `dist/` as static assets. It has no account or route yet; add both when the +site has a home, and build with `DOCS_SITE_URL` set to its origin. diff --git a/packages/docs/astro.config.ts b/packages/docs/astro.config.ts new file mode 100644 index 000000000..a72d99fd9 --- /dev/null +++ b/packages/docs/astro.config.ts @@ -0,0 +1,77 @@ +import { defineConfig } from 'astro/config'; +import tailwindcss from '@tailwindcss/vite'; +import nimbus, { defineConfig as defineNimbusConfig } from '@cloudflare/nimbus-docs'; +import { tableScroll } from '@cloudflare/nimbus-docs/markdown'; + +const REPO = 'https://github.com/cloudflare/forge'; + +const nimbusConfig = defineNimbusConfig({ + // Nimbus requires a canonical origin: it drives canonical URLs, the absolute OG image URLs, + // robots.txt, the sitemap and the links inside /llms.txt. Set `DOCS_SITE_URL` for any build that + // is deployed; the fallback is only good for local work. + site: process.env.DOCS_SITE_URL ?? 'http://localhost:4321', + title: 'Forge', + description: + 'A pluggable, schema-first code generator. Forge reads an OpenAPI document into one typed model and hands it to plugins that write SDKs, CLIs, MCP servers and docs.', + locale: 'en', + github: REPO, + editPattern: `${REPO}/edit/main/packages/docs/{path}`, + socialImageAlt: 'Forge documentation', + // Browser chrome follows the scheme. Dark is the answer to "no preference"; BaseLayout's bootstrap + // reconciles these once it has resolved the mode. + head: [ + { + tag: 'meta', + attrs: { name: 'theme-color', content: '#ffffff', media: '(prefers-color-scheme: light)' }, + }, + { + tag: 'meta', + attrs: { name: 'theme-color', content: '#0d0d0d', media: '(prefers-color-scheme: dark)' }, + }, + ], + // Groups follow the reading order of the project, not the directory tree. `sidebar.order` in each + // page's frontmatter decides its position within its group. + sidebar: { + items: [ + { label: 'Start Here', autogenerate: { directory: 'start' } }, + { label: 'Core Concepts', autogenerate: { directory: 'concepts' } }, + { label: 'Writing Plugins', autogenerate: { directory: 'plugins' } }, + { label: 'Targets', autogenerate: { directory: 'targets' } }, + { label: 'Authoring OpenAPI', autogenerate: { directory: 'openapi' } }, + { label: 'Compatibility Modes', autogenerate: { directory: 'compatibility' } }, + { label: 'Guides', autogenerate: { directory: 'guides' } }, + { + label: 'Reference', + items: [{ autogenerate: { directory: 'reference' } }, { label: 'Releases', link: `${REPO}/releases` }], + }, + ], + }, +}); + +export default defineConfig({ + output: 'static', + // Tailwind v4 through its Vite plugin, which is what Astro 7 wants. + vite: { + plugins: [tailwindcss()], + }, + // Pull the next page into the HTTP cache while the pointer rests on its link. Paired with the + // cache policy in `public/_headers`, without which a prefetched page cannot be reused. + prefetch: { + prefetchAll: true, + defaultStrategy: 'hover', + }, + integrations: [ + nimbus(nimbusConfig, { + // Frontmatter has to validate for a page to render, and a broken internal link is a 404 for a + // reader; both are build failures rather than warnings. + rules: { + 'nimbus/frontmatter-shape': 'error', + 'nimbus/internal-link': 'error', + }, + markdown: { + // Wide tables scroll instead of overflowing the measure. + hastPlugins: [tableScroll()], + }, + }), + ], +}); diff --git a/packages/docs/fonts/Inter-Bold.ttf b/packages/docs/fonts/Inter-Bold.ttf new file mode 100644 index 000000000..9fb9b751e Binary files /dev/null and b/packages/docs/fonts/Inter-Bold.ttf differ diff --git a/packages/docs/nimbus.json b/packages/docs/nimbus.json new file mode 100644 index 000000000..1345330bd --- /dev/null +++ b/packages/docs/nimbus.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://nimbus-docs.com/schema/nimbus.json", + "version": "0.6.5", + "lastReviewedNimbusVersion": "0.18.0", + "templatesTag": "templates-v0.6.5", + "variant": "empty", + "registry": "https://nimbus-docs.com/registry", + "install": { + "root": "src", + "aliases": { + "@/*": "src/*" + } + }, + "components": [ + { + "slug": "link-card", + "type": "registry:ui", + "version": "0.8.2", + "source": "https://nimbus-docs.com/registry", + "hash": "sha256:50a1a703d1111aff309a92256dda1d8dbcb10ba92afd3ff8d4b2a92ac8aab069", + "files": ["src/components/ui/link-card/LinkCard.astro", "src/components/ui/link-card/index.ts"] + } + ] +} diff --git a/packages/docs/og-assets/og-mark.png b/packages/docs/og-assets/og-mark.png new file mode 100644 index 000000000..d817de4d1 Binary files /dev/null and b/packages/docs/og-assets/og-mark.png differ diff --git a/packages/docs/og-assets/og-mark.svg b/packages/docs/og-assets/og-mark.svg new file mode 100644 index 000000000..71bb5f10e --- /dev/null +++ b/packages/docs/og-assets/og-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/docs/package.json b/packages/docs/package.json new file mode 100644 index 000000000..f269127df --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,43 @@ +{ + "name": "forge-docs", + "version": "0.0.0", + "private": true, + "description": "Documentation website for Forge.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/forge", + "directory": "packages/docs" + }, + "type": "module", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "check": "astro check", + "typecheck": "astro check", + "lint:docs": "nimbus-docs lint", + "lint:docs:fix": "nimbus-docs lint --fix", + "astro": "astro" + }, + "dependencies": { + "@cloudflare/nimbus-docs": "^0.18.0", + "@fontsource-variable/inter": "5.3.0", + "@fontsource-variable/jetbrains-mono": "5.3.0", + "@iconify-json/ph": "^1.2.2", + "astro": "^7.2.8", + "astro-og-canvas": "^0.13.0", + "canvaskit-wasm": "^0.41.1", + "clsx": "^2.1.1", + "tailwind-merge": "^3.7.0" + }, + "devDependencies": { + "@astrojs/check": "^0.9.10", + "@tailwindcss/vite": "^4.3.3", + "pagefind": "^1.5.2", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "wrangler": "^4.127.0" + } +} diff --git a/packages/docs/public/_headers b/packages/docs/public/_headers new file mode 100644 index 000000000..f6adecc8d --- /dev/null +++ b/packages/docs/public/_headers @@ -0,0 +1,29 @@ +# Cache policy for the asset store. Parsed by Workers, never served itself. +# +# Workers' default for every static asset is `public, max-age=0, +# must-revalidate`, which is a safe default and the wrong one here in two ways. +# +# It made the hover prefetch useless. A response that must be revalidated +# cannot be reused from cache, so the document Astro had already fetched was +# thrown away and refetched on the click. Meanwhile the stylesheet -- also +# revalidated, but tiny and answered with a 304 -- was ready first, so the +# browser had enough to paint a styled header over bare background while the +# body was still arriving. That frame is the flash of black between pages. +# +# It also revalidates fingerprinted files on every navigation. Those URLs +# change whenever their content does, so the round trip can never do anything +# except confirm what the URL already promised. + +# Everything, unless overridden below. A minute of freshness is what makes a +# prefetched page reusable on the click; `must-revalidate` then forbids serving +# it stale, so a deploy is picked up a minute later at the outside. +/* + Cache-Control: public, max-age=60, must-revalidate + +# Astro fingerprints everything it emits here, so these are safe to keep for a +# year and never revalidate. The detach is deliberate: a request matching two +# rules inherits both, and a header set twice is joined with a comma rather +# than replaced, so the broader rule above has to be removed first. +/_astro/* + ! Cache-Control + Cache-Control: public, max-age=31536000, immutable diff --git a/packages/docs/public/favicon.svg b/packages/docs/public/favicon.svg new file mode 100644 index 000000000..bdd04a847 --- /dev/null +++ b/packages/docs/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/docs/src/components.ts b/packages/docs/src/components.ts new file mode 100644 index 000000000..bd3ba426b --- /dev/null +++ b/packages/docs/src/components.ts @@ -0,0 +1,42 @@ +/** + * MDX globals registry — components available inside MDX without `import`. + * Wired via `` in `[...slug].astro`, and + * parsed at build time by the MDX validator, which fails the build on a + * PascalCase tag that is neither registered here nor imported by the page. + */ + +import { Aside } from './components/ui/aside'; +import { Badge } from './components/ui/badge'; +import { Card } from './components/ui/card'; +import { CardGrid } from './components/ui/card-grid'; +import { LinkCard } from './components/ui/link-card'; +import { PackageManagers } from './components/ui/package-managers'; +import Render from './components/Render.astro'; +import { Step, Steps } from './components/ui/steps'; +import { Tabs, TabItem } from './components/ui/tabs'; + +// Ours, not Nimbus's. +import Features from './components/Features.astro'; +import Hero from './components/Hero.astro'; +import NavList from './components/NavList.astro'; +import PipelineFigure from './components/PipelineFigure.astro'; +import Prose from './components/Prose.astro'; + +export const components = { + Aside, + Badge, + Card, + CardGrid, + Features, + Hero, + LinkCard, + NavList, + PackageManagers, + PipelineFigure, + Prose, + Render, + Step, + Steps, + TabItem, + Tabs, +}; diff --git a/packages/docs/src/components/AgentDirective.astro b/packages/docs/src/components/AgentDirective.astro new file mode 100644 index 000000000..c2617dfc2 --- /dev/null +++ b/packages/docs/src/components/AgentDirective.astro @@ -0,0 +1,16 @@ +--- +interface Props { + /** Absolute or site-relative URL for this page's markdown version. */ + markdownUrl: string; + /** Absolute or site-relative URL for the top-level llms.txt index. */ + llmsUrl: string; +} + +const { markdownUrl, llmsUrl } = Astro.props; +--- + + diff --git a/packages/docs/src/components/Features.astro b/packages/docs/src/components/Features.astro new file mode 100644 index 000000000..4a2d5a613 --- /dev/null +++ b/packages/docs/src/components/Features.astro @@ -0,0 +1,146 @@ +--- +/** + * The landing page's feature grid: six claims, each backed by something in the code. + * + * Every cell is an icon, a title and a sentence. The claims are deliberately checkable -- each one + * names the type, flag or file that makes it true -- because a code generator's docs are read by + * people deciding whether to trust its output. + */ +import Icon from '@cloudflare/nimbus-docs/components/Icon.astro'; + +interface Feature { + icon: string; + title: string; + body: string; + href?: string; +} + +const features: Feature[] = [ + { + icon: 'ph:plugs-connected', + title: 'Plugins all the way down', + body: 'The TypeScript SDK and the MCP server are plugins. They implement the same two-method Plugin trait yours will, and get nothing yours does not.', + href: '/plugins/overview/', + }, + { + icon: 'ph:tree-structure', + title: 'One model, resolved once', + body: 'Groups, methods, models and types, with every $ref followed, every null folded into a union, and every model placed in the group that uses it.', + href: '/concepts/the-model/', + }, + { + icon: 'ph:file-ts', + title: 'Write plugins in TypeScript', + body: '@cloudflare/forge/plugin gives you a typed object graph and a printer that writes your imports. forge-plugin build compiles it to a 2 MB component.', + href: '/plugins/typescript/', + }, + { + icon: 'ph:gear-six', + title: 'Or in Rust', + body: 'Implement forge::Plugin in-tree, or compile against the WIT with wit-bindgen for wasm32-wasip2 and ship it as a file.', + href: '/plugins/rust/', + }, + { + icon: 'ph:shield-check', + title: 'Sandboxed by construction', + body: 'A WebAssembly plugin gets no files, network, environment or clock. It reads the model and returns files; the host checks every path before writing.', + href: '/concepts/sandbox/', + }, + { + icon: 'ph:lightning', + title: 'Fast, and parallel', + body: 'A Rust core with arena-backed data. Groups generate concurrently, each wasm group in its own instance, and every error in the document is reported at once.', + href: '/concepts/pipeline/', + }, +]; +--- + +
+ { + features.map((feature) => { + const Tag = feature.href ? 'a' : 'div'; + return ( + + + + +

{feature.title}

+

{feature.body}

+
+ ); + }) + } +
+ + diff --git a/packages/docs/src/components/Header.astro b/packages/docs/src/components/Header.astro new file mode 100644 index 000000000..6136d2db7 --- /dev/null +++ b/packages/docs/src/components/Header.astro @@ -0,0 +1,105 @@ +--- +import Icon from "@cloudflare/nimbus-docs/components/Icon.astro"; +import { Button } from "./ui/button"; +import { LinkButton } from "./ui/link-button"; +import { ThemeToggle } from "./ui/theme-toggle"; +import { SearchTrigger } from "./ui/search"; +import { config } from "virtual:nimbus/config"; +import { getSidebarSections } from "@cloudflare/nimbus-docs"; +import Logo from "./Logo.astro"; + +interface Props { + /** Astro collection id for the current page, forwarded from DocsLayout. */ + collection?: string; + /** Astro entry id for the current page, forwarded from DocsLayout. */ + entryId?: string; + /** + * Whether the page has a sidebar to open. When `false`, the mobile + * menu button is hidden — pages that opted out via `sidebar: false` + * shouldn't show a button that opens an empty dialog. Default `true`. + */ + showSidebar?: boolean; +} + +// `entryId` is part of Props as a forward-compat hook (the version-switcher +// recipe reads it) but the base Header doesn't use it — leave it off the +// destructure to avoid an unused-var warning. +const { collection, showSidebar = true } = Astro.props; + +// Normalize trailing slash so isActive matches sidebar hrefs. +const currentSlug = Astro.url.pathname.replace(/\/$/, "") || "/"; +const sections = await getSidebarSections(currentSlug, { collection }); +const showSections = sections.length >= 2; +--- + +
+
+
+ + + + + {showSections && ( + + )} +
+ +
+ {config.search !== false && } + {config.github && ( + + + + )} + + {showSidebar && ( +
+
+
+ + diff --git a/packages/docs/src/components/Hero.astro b/packages/docs/src/components/Hero.astro new file mode 100644 index 000000000..6dd1c10da --- /dev/null +++ b/packages/docs/src/components/Hero.astro @@ -0,0 +1,163 @@ +--- +/** + * The landing page hero. + * + * Nimbus has no splash template -- `mode: custom` gives a bare `
` and the page composes what + * it wants -- so the hero is an ordinary component used from the top of `index.mdx`. + * + * The layout is Cloudflare's own marketing pattern: an eyebrow, a large headline, one sentence of + * tagline, two buttons, and the figure underneath. The orange is spent on the primary button and + * the eyebrow's dot and nowhere else, so the call to action stays the brightest thing on screen. + */ +import { LinkButton } from '@/components/ui/link-button'; + +interface Props { + /** The `

`. */ + title: string; + tagline: string; + /** A short label above the headline. */ + eyebrow?: string; +} + +const { title, tagline, eyebrow } = Astro.props; +const hasFigure = Astro.slots.has('default'); +--- + +
+ +
+ { + eyebrow && ( +

+ + {eyebrow} +

+ ) + } +

{title}

+

{tagline}

+
+ + Get started + + + Write a plugin + + + GitHub + +
+ { + hasFigure && ( +
+ +
+ ) + } +
+
+ + diff --git a/packages/docs/src/components/Logo.astro b/packages/docs/src/components/Logo.astro new file mode 100644 index 000000000..b9b9637ce --- /dev/null +++ b/packages/docs/src/components/Logo.astro @@ -0,0 +1,60 @@ +--- +/** + * The site's lockup: the Cloudflare cloud and the word "Forge". + * + * The cloud is Cloudflare's own mark, in its own two oranges, and is never recoloured -- the + * same paths as `public/favicon.svg`. The word is live text in the site's sans at a heavy + * weight, so it follows the scheme like any other heading and needs no generated artwork. + * + * `size` scales the whole lockup through `font-size`; everything inside is in `em`. + */ +interface Props { + /** Font size of the word, which the cloud is sized against. */ + size?: string; + class?: string; +} + +const { size = '1.125rem', class: className } = Astro.props; +--- + + + + Forge + + + diff --git a/packages/docs/src/components/NavList.astro b/packages/docs/src/components/NavList.astro new file mode 100644 index 000000000..1f8b5ee62 --- /dev/null +++ b/packages/docs/src/components/NavList.astro @@ -0,0 +1,88 @@ +--- +/** + * A clean, hairline-divided list of links -- the landing's alternative to a + * stack of filled cards. Transparent rows, one border around the group, an + * immediate hover tint, and an arrow that nudges. Pairs with the bento above. + */ +import Icon from "@cloudflare/nimbus-docs/components/Icon.astro"; + +interface Item { + title: string; + description?: string; + href: string; +} +interface Props { + items: Item[]; +} +const { items } = Astro.props; +--- + + + + diff --git a/packages/docs/src/components/PipelineFigure.astro b/packages/docs/src/components/PipelineFigure.astro new file mode 100644 index 000000000..b04b6c605 --- /dev/null +++ b/packages/docs/src/components/PipelineFigure.astro @@ -0,0 +1,294 @@ +--- +/** + * The landing figure: what goes in, what Forge builds, and what plugins make of it. + * + * It is plain HTML rather than a canvas, so every label in it is real text: selectable, measured + * by contrast tools, and read by a screen reader in order. The connectors are the only decoration, + * and they animate only when the reader has not asked for reduced motion. + * + * The contents are true to the code. The model column lists what `forge::Forge` exposes, and the + * plugin column lists the `--target` values the `forge` command accepts. + */ +import Icon from '@cloudflare/nimbus-docs/components/Icon.astro'; + +const plugins = [ + { target: 'ts', title: 'TypeScript SDK', note: 'client.ts, models.ts, index.ts per group' }, + { target: 'mcp', title: 'MCP server', note: 'a Rust crate with one tool per method' }, + { target: 'wasm://plugin.wasm', title: 'Your plugin', note: 'Rust, TypeScript, or any component' }, +]; +--- + +
+
+ Forge reads an OpenAPI document, builds one typed model of its groups, methods, models and + types, and hands that model to a plugin. Built-in plugins generate a TypeScript SDK or an MCP + server; a plugin compiled to WebAssembly can generate anything else. +
+ +
+

01 Input

+
+

openapi.json

+
"/accounts/{id}/members": {
+  "get": {
+    "operationId":
+      "accounts.members.list",
+    "summary": "List members"
+  }
+}
+
+
+ + + +
+

02 Model

+
+

forge::Forge

+
    +
  • group accounts
  • +
  • group members
  • +
  • method list GET
  • +
  • model Member
  • +
  • type object { id, email, … }
  • +
+
+
+ + + +
+

03 Plugins

+
    + { + plugins.map((plugin) => ( +
  • + --target {plugin.target} + {plugin.title} + {plugin.note} +
  • + )) + } +
+
+
+ + diff --git a/packages/docs/src/components/Prose.astro b/packages/docs/src/components/Prose.astro new file mode 100644 index 000000000..59769ccd5 --- /dev/null +++ b/packages/docs/src/components/Prose.astro @@ -0,0 +1,18 @@ +--- +/** + * The prose container a `mode: custom` page has to bring itself. + * + * `mode: custom` gives the page a bare `
`: no sidebar, no table of contents, and -- the part + * that is easy to miss -- no `.docs-content` wrapper and no width cap either. Every prose rule in + * `styles/prose.css` is scoped to `.docs-content`, so without this the landing page's body text + * renders unstyled and edge to edge. + * + * Width matches a doc page's text measure: 784px, filled edge to edge. Mobile keeps a small gutter; + * from `lg` up the horizontal padding is dropped so the text is the full 784px rather than 784 minus + * padding. + */ +--- + +
+ +
diff --git a/packages/docs/src/components/Render.astro b/packages/docs/src/components/Render.astro new file mode 100644 index 000000000..4ec3eefd2 --- /dev/null +++ b/packages/docs/src/components/Render.astro @@ -0,0 +1,106 @@ +--- +/** + * Render — include a reusable partial in any docs page. + * + * + * + * + * Partials live in `src/content/partials/` as MDX. Declare params in + * partial frontmatter (`params: [runtime, version?]`); required params + * fail at build time, optional use a `?` suffix. + */ +import { getCollection, getEntry, render } from "astro:content"; +import { components } from "../components"; + +interface Props { + /** Partial ID — path relative to src/content/partials/ without extension. */ + file: string; + /** Parameters passed to the partial as props. */ + params?: Record; +} + +const { file, params } = Astro.props; +const page = Astro.url.pathname; +const partial = await getEntry("partials", file); + +if (!partial) { + const allPartials = await getCollection("partials"); + const partialIds = allPartials.map((p) => p.id); + const hint = closest(file, partialIds); + const shortList = partialIds.sort().slice(0, 10).join(", "); + const tail = + partialIds.length > 10 + ? ` (and ${partialIds.length - 10} more)` + : partialIds.length === 0 + ? "none" + : ""; + throw new Error( + `[Render] Partial "${file}" not found, included on "${page}".` + + (hint ? ` Did you mean "${hint}"?` : "") + + ` Available: ${shortList}${tail}`, + ); +} + +const declaredParams = partial.data.params; +if (declaredParams) { + const required = declaredParams.filter((param: string) => !param.endsWith("?")); + const optional = declaredParams.filter((param: string) => param.endsWith("?")); + const allNames = [...required, ...optional.map((param: string) => param.slice(0, -1))]; + const received = Object.keys(params ?? {}); + + const missing = required.filter((param: string) => !received.includes(param)); + if (missing.length > 0) { + throw new Error( + `[Render] Missing required params ${JSON.stringify(missing)} for "${file}" on "${page}". ` + + `Expected: ${JSON.stringify(declaredParams)}, received: ${JSON.stringify(received)}`, + ); + } + + const unexpected = received.filter((param) => !allNames.includes(param)); + if (unexpected.length > 0) { + const unexpectedHints = unexpected + .map((u) => { + const h = closest(u, allNames); + return h ? `"${u}" (did you mean "${h}"?)` : `"${u}"`; + }) + .join(", "); + throw new Error( + `[Render] Unexpected params ${unexpectedHints} for "${file}" on "${page}". ` + + `Declared: ${JSON.stringify(declaredParams)}`, + ); + } +} + +const { Content } = await render(partial); + +/** Levenshtein distance — inlined to keep this component dep-free. */ +function distance(a: string, b: string): number { + if (a === b) return 0; + if (!a.length) return b.length; + if (!b.length) return a.length; + const v0 = new Array(b.length + 1); + const v1 = new Array(b.length + 1); + for (let i = 0; i <= b.length; i++) v0[i] = i; + for (let i = 0; i < a.length; i++) { + v1[0] = i + 1; + for (let j = 0; j < b.length; j++) { + const cost = a[i] === b[j] ? 0 : 1; + v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost); + } + for (let j = 0; j <= b.length; j++) v0[j] = v1[j]; + } + return v1[b.length]; +} + +function closest(target: string, candidates: string[], maxDist = 3): string | null { + const t = target.toLowerCase(); + let best: { name: string; dist: number } | null = null; + for (const c of candidates) { + const d = distance(t, c.toLowerCase()); + if (d <= maxDist && (!best || d < best.dist)) best = { name: c, dist: d }; + } + return best?.name ?? null; +} +--- + + diff --git a/packages/docs/src/components/ui/aside/Aside.astro b/packages/docs/src/components/ui/aside/Aside.astro new file mode 100644 index 000000000..5635f959d --- /dev/null +++ b/packages/docs/src/components/ui/aside/Aside.astro @@ -0,0 +1,76 @@ +--- +import Icon from "@cloudflare/nimbus-docs/components/Icon.astro"; +import { cn } from "@/lib/cn"; +import type { HTMLAttributes } from "astro/types"; + +interface Props extends HTMLAttributes<"aside"> { + // Keeps `type` (not `variant`) for Starlight compatibility — Starlight's