{"owner":"middleapi","repo":"orpc","hasSkills":true,"totalSkillsCount":56,"totalTokensCount":393304,"categories":["anthropic-skill","claude-rule","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/blume-migrate/SKILL.md",".agents/skills/blume-migrate/references/docsify.md",".agents/skills/blume-migrate/references/docus.md",".agents/skills/blume-migrate/references/docusaurus.md",".agents/skills/blume-migrate/references/fern.md",".agents/skills/blume-migrate/references/fumadocs.md",".agents/skills/blume-migrate/references/gitbook.md",".agents/skills/blume-migrate/references/github-wiki.md",".agents/skills/blume-migrate/references/jekyll.md",".agents/skills/blume-migrate/references/mdbook.md",".agents/skills/blume-migrate/references/mintlify.md",".agents/skills/blume-migrate/references/mkdocs.md",".agents/skills/blume-migrate/references/monorepo.md",".agents/skills/blume-migrate/references/nextra.md",".agents/skills/blume-migrate/references/readme.md",".agents/skills/blume-migrate/references/redocly.md",".agents/skills/blume-migrate/references/starlight.md",".agents/skills/blume-migrate/references/vitepress.md",".agents/skills/blume-migrate/references/vuepress.md",".agents/skills/blume-update-docs/SKILL.md",".agents/skills/blume-update-docs/references/audit-checklist.md",".agents/skills/blume/SKILL.md","CLAUDE.md","packages/ai-sdk/README.md","packages/arktype/README.md","packages/bun/README.md","packages/client/README.md","packages/cloudflare/README.md","packages/contract/README.md","packages/effect/README.md","packages/evlog/README.md","packages/hibernation/README.md","packages/json-schema/README.md","packages/lock/README.md","packages/mcp/README.md","packages/msw/README.md","packages/nest/README.md","packages/next/README.md","packages/node/README.md","packages/openapi/README.md","packages/opentelemetry/README.md","packages/pinia-colada/README.md","packages/pino/README.md","packages/publisher/README.md","packages/ratelimit/README.md","packages/server/README.md","packages/shared/README.md","packages/swr/README.md","packages/tanstack-query/README.md","packages/trpc/README.md","packages/valibot/README.md","packages/zod/README.md","skills/orpc-contract/SKILL.md","skills/orpc-migrate/SKILL.md","skills/orpc-openapi/SKILL.md","skills/orpc/SKILL.md"],"skills":{".agents/skills/blume-migrate/SKILL.md":"---\nname: blume-migrate\ndescription: Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, VitePress, VuePress, Docus, MkDocs, mdBook, Fern, GitBook, ReadMe, Redocly, Docsify, Jekyll (Just the Docs), GitHub Wiki, or any docs framework) to Blume, the markdown-first docs framework on Astro. Translate the source config to blume.config.ts, restructure content into Blume's filesystem-derived navigation, rewrite JSX callouts to directives, convert icons to Lucide, and inline snippets. Use when the user asks to migrate/convert/port a docs repo to Blume, or when the repo has a docs.json/mint.json, docusaurus.config.*, meta.json with fumadocs, _meta.* with nextra, an astro.config.* with starlight(), a .vitepress/config.* or .vuepress/config.*, an mkdocs.yml or zensical.toml, a book.toml (mdBook), a fern/docs.yml, a GitBook .gitbook.yaml/gitbook-docs.yaml, ReadMe _order.yaml files in docs/ or reference/, a Redocly sidebars.yaml or @redocly/realm dependency, an index.html that sets window.$docsify, a docus or @nuxt-themes/docus dependency, a Jekyll _config.yml (Just the Docs theme), or a GitHub wiki clone (Home.md with _Sidebar.md, [[Page Name]] links).\n---\n\n# Migrate to Blume\n\nBlume is a **markdown-first** documentation framework on Astro/Vite. You drop Markdown/MDX into a folder and get navigation, search, theming, Open Graph images, and a component library with no app boilerplate — **the framework is the template**. There is no starter to clone; the only thing a project owns is its content and a `blume.config.ts`.\n\nYour job is to convert a source docs repo into an **idiomatic** Blume project — not a 1:1 transliteration. Read this file, detect the source framework, open the matching `references/<framework>.md` for the exact mappings, and work the loop below. Report everything you drop or approximate.\n\nThroughout this skill (including the `references/` files), **`<skill>` means the absolute path of the directory containing this SKILL.md** — resolve it from wherever you read this file (e.g. `node_modules/blume/skills/blume-migrate` or `.claude/skills/blume-migrate`). It is a placeholder to substitute, never a literal path.\n\n## Migration philosophy\n\n- **Target idiomatic Blume, not a mechanical port.** Prefer filesystem-derived navigation over an exhaustive explicit `navigation.sidebar`. Prefer `:::` directives over JSX callouts. Prefer Blume defaults over restating them in config.\n- **Every field has a default; `{}` is a valid config.** Map only what the source _declares_. If the source uses a framework default, don't write it.\n- **Drop chrome that has no Blume equivalent — and say so.** Navbar CTAs, custom theming, conditional redirects, and unmappable icons get reported to the user, not silently discarded or faked.\n- **Convert, don't preserve.** Blume's page frontmatter schema is **strict** — unknown keys are build errors. A source-only frontmatter key must be mapped to a Blume key or removed (and reported), never left to \"maybe validate.\"\n\n## Migration workflow\n\n1. **Detect the source framework** and read its reference file:\n   - `docs.json` / `mint.json` → **Mintlify** (`references/mintlify.md`) — the deepest, config-declared nav.\n   - `docusaurus.config.*` → **Docusaurus** (`references/docusaurus.md`).\n   - `meta.json` + `fumadocs-*` deps (content under `content/docs/`) → **Fumadocs** (`references/fumadocs.md`).\n   - `_meta.{js,ts,json}` + `nextra` deps → **Nextra** (`references/nextra.md`).\n   - `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).\n   - `.vitepress/config.{js,ts,mjs,mts}` (often `docs/.vitepress/`) or a `vitepress` dependency → **VitePress** (`references/vitepress.md`). `.vuepress/` is VuePress (next bullet).\n   - `.vuepress/config.{js,ts,mjs}` (v1 also `config.yml`/`config.toml`; often `docs/.vuepress/`), a `vuepress.config.*`, or a `vuepress` dependency → **VuePress** (`references/vuepress.md`). It builds on `references/vitepress.md` for the Markdown layer the two share, so read both.\n   - A `docus` dependency (often in `docs/package.json` beside a `nuxt.config.ts` with `extends: ['docus']`), or `@nuxt-themes/docus` for 1.x → **Docus** (`references/docus.md`). Pages are MDC, Nuxt Content's component syntax: `::note` … `::` is not a Blume directive, and Blume prints it as text (`BLUME_MDC_SYNTAX` warns at its line).\n   - `mkdocs.yml`/`mkdocs.yaml` (also Zensical's `zensical.toml` and ProperDocs' `properdocs.yml`) → **MkDocs / Material for MkDocs** (`references/mkdocs.md`): nav declared in config, Python-Markdown extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets), and Python deps, so usually no `package.json`. Run `node <skill>/scripts/mkdocs-codemod.mjs --write <content-dir>` first, then `pin-heading-ids.mjs` after the first build.\n   - `fern/docs.yml` (beside `fern/fern.config.json`) → **Fern** (`references/fern.md`). A `fern/` folder without `docs.yml` only generates SDKs: the docs are elsewhere.\n   - `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, or a `SUMMARY.md` beside a `.gitbook/` folder → **GitBook** (`references/gitbook.md`). `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit (same reference, its own section); `book.toml` + `src/SUMMARY.md` is mdBook (`references/mdbook.md`).\n   - `book.toml` (at the book root, often `docs/`, `book/`, or `guide/` in a code repo; chapters in its `[book] src`, beside a `SUMMARY.md`) → **mdBook** (`references/mdbook.md`). Every URL ends in `.html`, `SUMMARY.md` is the whole nav, and `{{#include}}` pulls anchored code from across the repo: build the old book first, since it's the URL list, the old ids, and the only record of what the includes showed.\n   - `sidebars.yaml` or an `@redocly/realm`-family dependency, usually beside a `redocly.yaml` and with Markdoc `{% … %}` tags in pages → **Redocly** (`references/redocly.md`). GitBook pages use `{% … %}` tags too, so check for GitBook's files first. A `redocly.yaml` holding only lint/bundle keys (`extends`, `rules`, `decorators`, `apis`) is Redocly CLI config, not a docs site: keep it and detect the framework from the other signals.\n   - `_order.yaml` files in `docs/`, `reference/`, `recipes/`, or `custom_pages/` (ReadMe's bi-directional Git sync), or a CI step running `readmeio/rdme` beside pages with `category`/`parentDoc` (or `category.uri`/`parent.uri`) frontmatter (`rdme` uploads) → **ReadMe** (`references/readme.md`). ReadMe keeps settings, redirects, the glossary, and variables in its dashboard, not the repo, and changelogs only on the `main` branch.\n   - An `index.html` setting `window.$docsify` (usually in `docs/`, beside `_sidebar.md`), or `docsify-cli` in `package.json` → **Docsify** (`references/docsify.md`). Run `node <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old --group-folders <docs-dir>` first, then `pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json` after the first build. Its default URLs live after `#` (`/#/page`), which no server-side redirect can match; the reference adds a client-side script.\n   - `_config.yml` (root or `docs/`) → **Jekyll** (`references/jekyll.md`); Just the Docs is `just-the-docs` in `_config.yml` or the `Gemfile`. Build the old site first with a project-local bundle: the nav comes from front matter (`parent`/`nav_order`) and is rebuilt from the old build's sidebar, and URLs end in `/` or `.html`. A `_toc.yml` beside it is Jupyter Book, not Jekyll. Run `node <skill>/scripts/jekyll-codemod.mjs --old <old build> --write .` from the Jekyll source dir first, then `pin-heading-ids.mjs --old <old build>` after the first build.\n   - A clone of `<repo>.wiki.git` (`Home.md` at the root, often `_Sidebar.md` and `_Footer.md`, `[[Page Name]]` links, no config) → **GitHub Wiki** (`references/github-wiki.md`). Never detected: the source is named. Its old URLs stay on github.com, which can't redirect, so the reference writes a route table and link stubs for the old wiki instead of `redirects`. Run `node <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write` first, classify `wiki-routes.json`, then `convert --repo <owner>/<repo> --write`.\n   - Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.\n   - **Also note the host repo, independent of source framework:** a pnpm/Turbo workspace, a non-`docs/` content layout, or a Vercel deploy each need integration steps (`content.root` scoping, `minimumReleaseAge`, lockfile, `vercel.json`, an Astro/Vite patch) — all in `references/monorepo.md`. Read it whenever the target isn't a bare single-package docs folder.\n2. **Inventory the repo** before changing anything (a page whose body is only a `<meta http-equiv=\"refresh\">` is a redirect, not a page: add the redirect from its old URL and report it): the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs and GraphQL schemas, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted. Pages the old site published by accident (a `.github/` template, files under dot-folders, planning notes) aren't content: leave them out, add no redirects, and list them in the report.\n3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: \"…\" })`.\n4. **Restructure content.** Put `blume.config.ts` in the directory whose `package.json` runs the old docs build, which may be a workspace package such as `packages/docs` rather than the repo root. Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: \".\"` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** A group that exists only in the source's nav config, with its pages flat on disk, becomes a **`(group)/` folder**: it adds the sidebar group and keeps every URL, with no redirects (moving pages a folder deeper instead changes their URLs and breaks their relative asset paths). Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.\n5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; turn snippets/partials into `<include>` statements or inline them (Blume has no import-based includes: `import Snippet from \"…\"` plus `<Snippet />` has to become one or the other; when a source includes part of a file (mdBook `ANCHOR`s, VitePress `#region`s, MkDocs snippet line ranges) or a file outside the content root, copy `<skill>/scripts/include-excerpts.mjs` into the project and run it before every `dev`, `build`, and `validate` rather than pasting code the project compiles: it reports every missing anchor or region in one pass, and a restored marker in the source beats a line range, which goes stale silently); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources — and links inside OpenAPI descriptions, which Blume renders and validates, though a redirect `from` hides a stale one; rewrite links that hardcode the old site's own origin to root-relative paths); **add a `redirects` entry for every route you moved** in step 4 (a hash-routed source like Docsify also needs its reference's client-side redirect, because `redirects` only see paths); **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`), and fix links to its anchor (`#<h1-slug>`), which no longer exists: drop the fragment. `blume validate --strict` also reports fragments the old site never checked; fix each or drop it, and report it. A page with no frontmatter `title` takes its opening `# H1` as the title and shows it once, so that one can stay; moving it into `title` is still the idiomatic form. **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `githubReleases()` source** (see \"Changelogs\" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`). For **Docus**, run the bundled codemod first: `node <skill>/scripts/docus-codemod.mjs --write content` pairs MDC blocks as remark-mdc does, converts the Nuxt UI prose components, fence labels, icons, frontmatter, and `.navigation.yml` files, makes the MDX prose safe, and lists every `REVIEW` item (custom and landing-page components stay as written; check them against the live site, not only a local build; see `references/docus.md`). For **VitePress**, run the bundled codemod first: `node <skill>/scripts/vitepress-codemod.mjs --write <srcDir>` converts containers (paired as markdown-it pairs them), code groups, alerts, fence labels, `<<<` imports and includes, `.html` links, badges, and H1 titles, and lists every `REVIEW` item for you to finish (see `references/vitepress.md`). Before converting any source's components, check its built output: a component its theme never registered rendered nothing there (`<!---->`), so converting it shows content readers never saw; report it. For **mdBook**, convert every `{{#…}}` helper before anything else: anchored, line-range, and out-of-root code becomes generated excerpts, one-line files become `variables`; then strip hidden `# ` lines and `rust,…` fence attributes, and convert admonish blocks (see `references/mdbook.md`). For **Jekyll**, convert every Kramdown IAL (`{: .note }`) and Liquid tag: in `.md` they ship as literal text (a Liquid tag warns `BLUME_TEMPLATE_TAG`, an IAL `BLUME_MD_ATTRIBUTE_LIST`), and in `.mdx` they fail the build (`blume check` names each IAL's line as `BLUME_MDX_ATTRIBUTE_LIST` and a Liquid tag's as `BLUME_MDX_SYNTAX`, at the partial's own line for one in an included partial). Run the bundled codemod first: it converts callout IALs, the in-page TOC, H1s, front matter, Markdown includes, and Liquid links and variables, rebuilds the Just the Docs sidebar as folders, `slug`s, and `meta.ts`, and writes the redirects and variables to `jekyll-migration.json` (see `references/jekyll.md`). For a **GitHub Wiki**, run the bundled codemod (`routes`, then `convert`): it builds the route table (one flat route per page name) and converts every `[[…]]` link outside code, which Blume would otherwise render as literal text (`BLUME_WIKILINK_UNSUPPORTED` warns only for one that names a page or has a `|`; Markdown pages put the link text first: `[[text|Page]]`). For **Redocly**, convert every Markdoc `{% … %}` tag: Blume never parses them, so an unconverted tag ships as literal text in `.md` (`BLUME_TEMPLATE_TAG` warns at its line) and fails an `.mdx` build. For **VuePress**, rename every `README.md` to `index` first, then run the VitePress codemod (its limits on VuePress content are in `references/vuepress.md`), then grep for hyphenated theme tags (`<code-group>`, `<code-block>`, `<router-link>`): they pass through `.mdx` as unknown HTML elements with no diagnostic. For **MkDocs**, decide `.md` vs `.mdx` per page before converting: MDX rejects HTML comments, `<https://…>` autolinks, unclosed void tags, and `{…}` attribute lists that Python-Markdown accepts (`blume check` names each at its line: `BLUME_MDX_SYNTAX` for what MDX can't parse, and the last two as `BLUME_MDX_UNCLOSED_ELEMENT` and `BLUME_MDX_ATTRIBUTE_LIST`), and snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), not the page (see `references/mkdocs.md`). For **Fern**, URLs come from `docs.yml`, not file paths: run `node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt` before moving anything (dry run, then `--write`), and after the first build its `endpoints` step, which joins `operation-routes.mjs` output to Fern's old API URLs and writes `fern-redirects.json`; export a Fern Definition to OpenAPI with `fern export` (see `references/fern.md`). For **GitBook**, put each page at its old URL first (GitBook's URLs follow `SUMMARY.md`, not the file tree), then convert every `{% … %}` tag: in `.md` an unconverted tag ships as literal text (`BLUME_TEMPLATE_TAG` warns), and in `.mdx` it fails the build. For **ReadMe**, run the bundled codemod first: `node <skill>/scripts/readme-codemod.mjs --write --site <hub origin> --glossary <saved hub page> .` keeps every page at its flat URL, converts callouts, `doc:`/`ref:` links (`blume validate` names any left as `BLUME_UNSUPPORTED_LINK_SCHEME`), ReadMe components, and frontmatter, writes `meta.ts`, overlays, and redirects, and lists what needs a person; after the first build, its `--routes` pass adds the endpoint redirects and per-tag order (see `references/readme.md`). A pasted widget's JSX event handler (`onClick={…}`) builds green and never runs on a static page: `BLUME_MDX_EVENT_HANDLER` names each one, so move the widget into an island (`islands/`) or drop it and report it.\n6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. Yarn's `npmMinimalAgeGate` refuses a fresh `blume` the same way: add it to `npmPreapprovedPackages`. **Always regenerate the lockfile in the same change** with the repo's own package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, or `bun install`; check `packageManager` and the lockfile's name) and commit it alongside `package.json` — CI and hosts install from a frozen lockfile, so a stale one fails the build before it starts. After creating the first new file in the content root (a `meta.ts`, a partial), run `git check-ignore -v <content root>/meta.ts`: a repo `.gitignore` rule that covers the content root leaves every new file out of `git add` while the old pages stay tracked, so fix the rule or report that each new file needs `git add -f`. Blume needs **Node 22.19 or later**: update `.nvmrc`/`.node-version`, `engines.node`, and any CI `node-version` in the same change. pnpm 11 and later run a dependency's build script only once `pnpm-workspace.yaml` approves it: add `allowBuilds: { esbuild: true }` (plus `\"@scarf/scarf\": false`, as `blume init` writes) unless the workspace already lists `esbuild`, keeping its other entries. Add `.blume/`, `dist/`, and `.env.local` to the docs package's own `.gitignore` when it has one, else the root's (as `blume init` does; a `vercel()` build appends `.vercel/` itself), and remove any line that ignores `public/` before moving assets into it. **`blume build` empties `<project>/dist`:** if the host repo already uses `dist/` for something else (Python wheels, a library build), put the Blume project in its own folder (`docs/`). If the repo formats with Prettier, extend its `*.md` overrides (`.prettierrc`, `.editorconfig`) to `*.mdx` and keep `{/* */}` comments on one line. After renaming or moving pages, grep the whole repo (release config, CI, bot and ignore files) for their old paths. Check any `tsconfig.json` in the Blume project dir too: Blume's build reads it, so an `extends` that resolves only after a root workspace install, or `include`s of the old framework's folders, fail `blume build`. Install the workspace first, fix the paths, or delete the file if only the old framework used it (a Nuxt `tsconfig.json` that `extends` or `references` `./.nuxt/*` fails with `BLUME_TSCONFIG_EXTENDS`, and a `postinstall: nuxt prepare` script goes with the Nuxt dependency). **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.71.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.\n7. **Wire up the host repo & deploy (non-trivial repos).** Settings a host keeps in its dashboard (Netlify's publish directory, build command, `NODE_VERSION`) can't be committed: list them in the summary. In `.gitignore`, replace only the docs' own entries and keep ones other packages use. For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`: with `outputDirectory: \"dist\"` for a static build, and with no `outputDirectory` for `deployment: vercel()`, whose build writes `.vercel/output` (add `.vercel/output/**` to Turborepo outputs). Tell the user the settings you can't commit (Vercel Root Directory, Node 22, and server secrets such as `AI_GATEWAY_API_KEY`). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.\n8. **Verify.** Run `blume build` (frontmatter schema, duplicate routes, config — it fails on any error diagnostic by default; **never pass `--no-strict`**, which builds anyway and silently drops pages with invalid front matter or `.mdx` that doesn't parse) and `blume validate --strict` (internal links, heading anchors, assets, redirect targets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).\n\n## The Blume mental model\n\nThe single biggest shift for most sources — especially Mintlify — is that **navigation is derived from the filesystem**, not declared in config.\n\n### Navigation is the file tree\n\n- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.\n- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).\n- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display, directory })`). The `pages` array lists children by slug (numeric prefix, parentheses, and extension stripped; an entry that names nothing warns `BLUME_META_UNKNOWN_PAGE` with the slug it likely meant, so write that one); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order. `directory` inherits down the tree: set `directory: \"none\"` on nested folders that shouldn't list cards. `pages` can't interleave loose pages with subgroups inside a `flat` group, or at the top of the sidebar in any mode: when a source interleaves them (mdBook, MkDocs `navigation.sections`), use `display: \"group\"`, where the order holds inside groups, and say what moved at the top level.\n- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `\"flat\"` (default), `\"group\"` (collapsible), or `\"page\"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).\n- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).\n- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting, **use a `(group)/` folder first**: move the pages into `<folder>/(<group>)/` with a `meta.ts` for the label, order, and `collapsed`. A parenthesized folder adds the sidebar group but no URL segment, so no route changes and no redirects are needed, and a page from another folder can join with a `slug` that keeps its old route. A group whose landing page is a sibling file keeps every URL too: move the landing page to `(group)/index.md` with `slug: <its old route>`, and the slugged index becomes the group's link. Moving a page a folder deeper breaks its relative links and assets (`./img.png`, `../other.md`), so re-base them. (Docsify is the exception: its old `#/` URLs are remapped by script whatever the paths, so its reference moves pages into real folders.) Only when that can't express the shape, move the pages into a real subfolder (URLs change: add `redirects`) or declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.\n\n### Tabs and selectors\n\n- **`navigation.tabs`** (`{ label, path, icon? }`) render top-of-header sections and **scope the sidebar by route** — the folder at a tab's `path` becomes the section, so this needs no config beyond the tabs themselves; structure content as **one folder per tab**. **A source's top-level tabs (Mintlify `navigation.tabs`, a top-level product/section switcher) map to these header tabs — keep them as tabs; don't flatten them into a single global `navigation.sidebar`.** Blume picks the active tab by **URL prefix** (longest tab `path` that prefixes the route), so every page in a tab must live under that tab's single `path`; a source tab that mixes arbitrary routes isn't portable as-is — either move its pages under one prefix (route change → add `redirects`) or accept the closest shape, and say which in the report (details in `references/mintlify.md`). The filtering runs both ways: on a route **under** a tab's `path`, the sidebar shows **only** that tab's folder (a tab also highlights when the current route is under it); on a **root or untabbed** route (or a tab whose `path` is `/`), the tab folders are **hidden** and the sidebar shows only the loose pages that belong to no tab (full tree as a fallback, so it's never blank). Consequence for migrations: once you add tabs, the landing sidebar automatically drops the sectioned content — that's intended, not lost pages; don't hand-build excludes for it.\n- **`navigation.selectors`** (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`, `kind` = `dropdown`/`product`/`version`/`language`) partition a whole site (products, versions) via a header dropdown keyed on the current route.\n- **`navigation.actions`** (`[{ label, href }]`) puts plain links in the **header**, left of the icon buttons, and **`navigation.cta`** (`{ label, href }`, singular) is the header's one filled call-to-action button. This is the home for a source's header bar — a \"Log in\"/\"Status\" link goes in `actions`, a \"Sign up\"/\"Get started\" button in `cta`. An `http(s)` href opens in a new tab; an internal route is validated against your pages at build time, so a route served by another app on the same host must be written as an absolute URL. Both hide on phones (below `sm`), except `cta` on a page with no navigation toggle; a link that must survive on a phone on docs pages belongs in `featured`.\n- **`navigation.featured`** (`{ label, href, icon? }`) pins links to the **top of the sidebar, above every section** — a blog, changelog, or support page that should always be one click away. These are the **exception to tab scoping**: unlike the generated tree, featured links show on **every** route and breakpoint. `href` points anywhere — an external URL opens in a new tab, an internal route (`/contact`) is validated against your pages at build time. `icon` is a Lucide name (or image path/URL/inline SVG), as everywhere else. This is the home for a source's always-visible header/utility links (Mintlify anchors, Blog/Contact links) — see `references/mintlify.md`.\n\n### Routes and pathing\n\n- A route is the content path relative to `content.root`, with **numeric prefixes stripped** (`01-intro.mdx` → `/intro`; a version or a name that starts with a date, `YYYY-MM-DD`, `D-M-YYYY`/`M-D-YYYY`, or `YYYY-MM`, stays whole: `12-05-2022.mdx` → `/12-05-2022`; pin `slug` for any other digit-led name the old site kept whole) and **`(group)/` folders adding no segment**. An `index` file maps to its folder's route. Frontmatter `slug` overrides the generated route. A `README.md` is an ordinary page at `/README`, not its folder's index: rename it `index.md` (Docsify, MkDocs, GitBook, and VuePress sources all use README as the index).\n\n### `blume.config.ts` shape\n\n`defineConfig({...})` — every field optional, all with defaults:\n\n- **Site:** `title`, `description`, `logo` (string SVG, or `{ image: string | { light, dark, alt }, text, href }`), `banner` (a string, or `{ content, link: { href, text }, dismissible, id }` — no color/type). A logo renders beside `title` in the header, so a **wordmark logo doubles the brand** (\"Acme Acme\") — set `text: \"\"` to render the mark alone. **Prefer the string form over `{ light, dark }`:** if you have the logo SVG locally and it's monochrome (solid black or white), rewrite its `fill`/`stroke` to `currentColor` and use `logo: \"/logo.svg\"` — it then inherits the theme's text color and adapts to light/dark automatically, so you don't need separate light/dark files.\n- **`theme`:** `accent` (a color string for both modes, or `{ light, dark }` per mode), `action` (color), `mode` (`light`/`dark`/`system`), `radius`, `fonts` (`{ body, display, mono }` — each a curated Google-font slug, a `{ name, provider?, weights? }` object for any Google/Fontsource/Bunny/Fontshare family, or `{ name, variants: [{ src, weight?, style? }] }` for local font files), `background` and `backgroundImage` (each a string, or `{ light, dark }` per mode). The old `accentDark`/`backgroundDark`/`backgroundImageDark` fields were **merged into these per-mode objects** — a bare string still applies to both modes, so only reach for `{ light, dark }` when the two modes differ. Each color must be a CSS color or an accent preset (a source's bare `\"3b82f6\"` needs its `#`), or `BLUME_THEME_COLOR_INVALID` warns and browsers ignore it. There is **no** `theme.strict` and **no** `theme.css` config field — custom CSS goes in a project-root **`theme.css` file** (auto-picked-up), and a source's \"strict appearance\" flags drop.\n- **`content`:** `root` (default `\"docs\"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `[\"**/*.{md,mdx}\"]` / `[\"**/_*\", \"**/.*\"]` — a set `exclude` **adds to** those defaults, so partials and dot-folders stay unpublished; to publish `_`-prefixed or dot-files on purpose, list the default with a `!`: `exclude: [\"!**/_*\"]`), `sources` (an array of **adapters imported from `blume/sources`**: `filesystem({ root, include, exclude })`, `obsidian({ vault })`, `githubReleases({ owner, repo })`, `notion({ database })`, `sanity({ projectId, dataset, query })`, `contentful({ space, contentType })`, `payload({ url, collection })`, `strapi({ url, contentType })`, `mdxRemote({ github })`, `custom(source)`; every factory with an options object also takes `prefix` and `pollInterval`, while `custom(source)` takes a `ContentSource` instance that sets its own `prefix`. The 1.x `{ type: \"…\" }` objects were removed — rename `type` to the factory call and pass the other fields as its options — except `{ type: \"custom\", source }`, which becomes `custom(source)` with the instance as the only argument. `root`/`include`/`exclude` are shorthand for a single `filesystem()` and are **rejected beside `sources`** — move them into the `filesystem()` entry. OpenAPI/AsyncAPI/GraphQL are **not** among these; they're adapters in the top-level `reference` list), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.\n- **`basePath`** (top-level): a site-wide mount point (e.g. `\"/docs\"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).\n- **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the footer's GitHub link when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see \"Config-declared nesting\" above).\n- **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: \"algolia\", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (the assistant, Open in chat — `ai.assistant.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openai({ model })` (or `openai({ baseUrl, name, model, apiKeyEnv })` for any OpenAI-compatible endpoint), `anthropic({ model })`, `gemini({ model })`, `grok({ model })`, `openrouter({ model, reasoning })`, `llmgateway({ model })`, or `inkeep({ model })`. Every adapter takes `model`, `apiKeyEnv`, `headers`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`). Every adapter except `inkeep()` also takes `reasoning`: Inkeep runs its own answer pipeline, so `inkeep({ reasoning })` fails validation with an unrecognized key — drop a source's reasoning setting there and report it. There are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.assistant` — a source that configured an AI assistant that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` sit on `ai.assistant` itself; there is **no** `ai.ask` — Blume 2.0.0 and earlier used that name, so an older `blume.config.ts` moves the whole block to `ai.assistant`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).\n- **`analytics`** — adapters from `blume/analytics`, including `script()` for a source's custom head scripts. A known analytics snippet in the source's head scripts (the gtag loader plus `gtag('config')` pair, GTM, Plausible, …) becomes that provider's factory, not `script()`. Adapters load **only in production builds and wait for `consent`** when it's configured, so a script a page needs to work (a support widget, a form embed) goes in a `PageFooter` layout slot component instead (`defineComponents`).\n- **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: \"static\"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (the assistant, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from \"blume/deploy\"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: \"/docs\" }` (see below for when to add `site`). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from \"blume/deploy\"`) when a config uses both.\n- **Keep the source's site URL as `deployment.site` unless the target host is one Blume auto-detects.** Blume fills `site` in from the platform's build environment only on **Vercel and Netlify** (their stable production domain), and uses the dev server's `localhost` URL during `blume dev`; on those two hosts leave it unset and let detection pick the deployed URL. Everywhere else — Cloudflare Pages (it exposes only the per-deploy `CF_PAGES_URL`), GitHub Pages, S3 or another static host, a custom CDN, Cloudflare Workers, a `node()` server — nothing detects it, and an unset `site` silently drops the sitemap, OG images, RSS feeds, the AI catalog, and absolute canonical URLs. So when the source config had a `url`/`site` field and the target isn't one of those two hosts, carry it over: `deployment: { site: \"https://…\" }` for a static build, or the `site` option of a host adapter (`node({ site })`). If you can't tell where the site will deploy, keep it and say so in the report.\n- **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png` or `apple-touch-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.\n\nThe schema is exported from `blume/schema`; the full field reference is in the `docs/configuration/` directory of the installed `blume` package (see \"Full documentation\" below for how to locate it).\n\n### Icons are Lucide, period\n\nBlume resolves **bare kebab-case [Lucide](https://lucide.dev) names** everywhere an icon is accepted — frontmatter `icon`, `sidebar.icon`, `meta.ts` `icon`, `navigation.tabs`/`selectors` icons, and `Card`/`Step`/`Icon`/etc. props. There is **no** FontAwesome or Tabler support and **no** `iconType` prop. Names must be **kebab-case** (`book-open`, not `BookOpen`) — a PascalCase React-component name (common in Fumadocs/lucide-react sources) does not resolve and renders nothing. When migrating a source that uses another icon set (Mintlify and Fern use FontAwesome), **map each name to its closest Lucide equivalent**; where none exists, drop the icon and report it. Verify a name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it.\n\n### Page frontmatter (strict — unknown keys are build errors)\n\n```yaml\n---\ntitle: Install # renders as the page H1 — remove any duplicate H1 in the body\ndescription: Install Blume and scaffold your first project.\ntype: doc # doc (default); blog and changelog drive feeds, other values only mean something under content.types\nsidebar:\n  label: Install # overrides title in the sidebar\n  order: 2\n  icon: download\n  badge: New\n  hidden: false\nseo:\n  title: …\n  description: …\n  image: /og/install.png\n  canonical: https://…\n  noindex: false\nsearch:\n  exclude: false\n  tags: [api]\nslug: install # override the generated route\ndraft: false\nlastModified: 2026-06-20 # pin the \"last updated\" date\n---\n```\n\nAlso valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog metadata), `deprecated`, and the top-level shorthands `hidden` (for `sidebar.hidden`) and `noindex` (for `seo.noindex`).\n\n### Authoring features (no imports needed in `.mdx`)\n\n- **The rich features are MDX-only.** Directives, `package-install`, mermaid, and math are wired into the MDX processor; in a plain `.md` file a `:::note` stays **literal text** — and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. **Rename any `.md` file that uses (or should use) these to `.mdx` during migration.** MDX is stricter prose than Markdown: any `<` followed by a non-space character that doesn't open a tag (`<7`, `<50 ms`, `<= 5`, `List<T>`) and a bare `{` or `}` fail the build, so escape them (`&lt;` or `\\<`, and `\\{`) in any page you rename. With `lastModified: \"git\"`, every page the migration edits shows the migration commit's date, so say so in the report when the site shows dates; a page it only renames or moves keeps its date (Blume follows renames). This bites hardest on Docusaurus/Starlight sources, whose `.md` content is full of `:::` admonitions. Plain Markdown (headings, tables, fenced code with titles/highlighting) is fine in `.md`.\n- **Callouts as directives:** `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::info`, `:::success`, with an optional title in brackets, which renders its inline Markdown: `:::warning[Heads up]`. Aliases `caution`→warning, `error`→danger, `important`→note, `warn`→warning. A callout that needs its own icon or tint is `<Callout type icon color title>` (a Lucide `icon`, any CSS `color`), which a directive can't set. **markdown-it sources (VitePress, VuePress, Docusaurus v2) write `::: tip Title`: with a space after the colons it's never a directive, even in `.mdx`, and renders as literal text (`.mdx` warns `BLUME_DIRECTIVE_SPACED_NAME` only for a callout name); inside an open `:::` container it closes that container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`); a title after the name (`:::tip Title`) is dropped too (`BLUME_DIRECTIVE_OPENING_TEXT`).** Normalize to `:::tip[Title]`. MDC's two-colon blocks (Docus, any Nuxt Content site: `::note` … `::`) are leaf directives to Blume and print as text (`BLUME_MDC_SYNTAX`), and MDC `[text]{.class}` spans fail the MDX build (`BLUME_MDX_ATTRIBUTE_LIST` names the line). GitHub alerts (`> [!NOTE]`, any of GitHub's five, `[!CAUTION]` as danger) render as callouts in `.mdx`, text after the marker as the title; in `.md` they stay quotes that show the marker (`BLUME_MD_GITHUB_ALERT`), so rename those pages to `.mdx`.\n- **No-import MDX components:** `Callout`, `Card`/`CardGroup`, `Columns`/`Column`, `Steps`/`Step`, `Tabs`/`Tab`, `Accordion`/`AccordionItem`, `Expandable`, `FileTree`, `Tree`/`Tree.Folder`/`Tree.File`, `CodeGroup`, `Frame`, `Panel`, `Tooltip`, `Tile`, `Badge`, `Icon`, `TypeTable`/`AutoTypeTable`, `Color`, `YouTube`, `Visibility`, `GithubInfo`, `Component`, `CodeBlock`, `Diff`, `Prompt`, `Math`, `Changelog`, and the hand-written endpoint fields `ParamField`/`ResponseField`/`RequestExample`/`ResponseExample` (Mintlify's semantics: `<ParamField path|query|header|body=\"name\">` names the field by its location; see `docs/references/api-pages.mdx`). (**Not** shipped — convert away: `<Warning>`/`<Note>`/`<Info>`/`<Tip>`/`<Check>`/`<Error>` → the matching `:::` directive, and `RequestField`. A `<CodeBlock>` with no `code` prop, used as a wrapper around a fence as Fern and Mintlify do, renders the fence as written but ignores its own `title`: unwrap the fence and put the title in its meta. A component that shows its children, used with none and another tool's props (`<Badge type=\"tip\" text=\"beta\" />`), renders empty and warns `BLUME_UNKNOWN_PROP`: put the content between the tags. See the reference files for targets.)\n- **Fenced-code superpowers:** ` ```package-install ` → package-manager tabs; ` ```mermaid ` → a rendered diagram; code-block titles (` ```ts server.ts `), line numbers (`lineNumbers`), and highlighting (`{1,4-5}`, `// [!code ++]`).\n- **Fences and titles that trip migrations:** a fence language Shiki doesn't know renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE` (case doesn't matter: `JSON` highlights like `json`): fix the id, or write `text` for plain text. A language glued to attributes with a comma (` ```rust,ignore `) is one of those; with a space instead, rustdoc's and mdBook's keywords (`ignore`, `no_run`, `should_panic`, `edition2021`, `editable`, …) warn `BLUME_CODE_FENCE_OPTION` and stay out of the title, but any other second word becomes the block's title: keep only the language. Other tools' fence options (`hl_lines=\"2 3\"`, `linenums=\"1\"`, `showLineNumbers`, Mintlify's `lines`, `wordWrap`, `filename=\"…\"`) do nothing, stay out of the title, and warn `BLUME_CODE_FENCE_OPTION` with the Blume spelling (`{2,3}`, `lineNumbers`, `wrap`, `title=\"…\"`). A fence with options but no language needs one (` ```text title=\"x\" lineNumbers `), or the first option is read as the language. A title is every word after the language, and a bracketed one (` ```ts [file.ts] `, from VitePress and MDC) drops its brackets, with a range glued after them (`[file.ts]{2}`) highlighting its lines. `Expandable`, `AccordionItem`, and callout titles render inline Markdown (code, emphasis, links); a `Step` title is a plain string that shows Markdown as written and has no anchor, so keep an anchored heading as the first line inside `<Step>`. Raw HTML `<img src>` and `<a href>` are never optimized: a relative one naming a file beside the page publishes that file with the page (as `[spec](./spec.pdf)` and `<Card img=\"./cover.png\">` do), a root path gains `deployment.base` like a Markdown link, and `blume validate` checks both, but a relative `<a href>` to a page resolves against the page's URL, not its folder. Convert images to Markdown so they're optimized, and page links to Markdown links or root routes.\n- **Math:** `$$…$$` renders in `.mdx` with **no conf\n\n<!-- [TRUNCATED AT 50KB] -->",".agents/skills/blume-migrate/references/docsify.md":"# Docsify → Blume\n\nDocsify has no build step. One `index.html` sets `window.$docsify = {…}`, loads Docsify and its plugins from a CDN, and renders each page's Markdown in the browser. Watch for five traps:\n\n- **URLs live after a `#`** (`/#/guide/setup?id=install`). A server never sees that part, so `redirects` can't match an old URL. The site needs a client-side redirect script (URLs below).\n- **Links resolve from the docs root**, not the page's folder, unless `relativePath: true`. Images and includes resolve from the page's folder either way, even with a leading `/`. Raw HTML `href`/`src` resolve from the docs root, because the document URL is always the folder holding `index.html`.\n- **Pages have no frontmatter.** The title is usually the first `# H1`, but not always.\n- **Navigation is a Markdown list** (`_sidebar.md`), and its groups usually sit flat in one folder.\n- **Heading ids differ from Blume's**, and old deep links carry them as `?id=`.\n\n**Fingerprint the version.** It changes callouts and heading ids. `docsify@4`, or any script URL with `/lib/` (even an unversioned `…/npm/docsify/lib/docsify.min.js`), runs v4 code: the v5 package still ships the v4 build under `lib/`. `docsify@5/dist/…` or `new Docsify({…})` is v5.\n\n## Detect\n\n- An **`index.html`** whose inline script sets **`window.$docsify`** (v5 can also use `new Docsify({…})`), usually in `docs/`. The folder holding it is the content root.\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `.nojekyll` beside it, with `README.md` as the home page.\n- `docsify-cli` or `docsify` in `package.json`. Without either, the source has to be named: `npx blume migrate docsify`.\n- A GitHub wiki has `_Sidebar.md` and `Home.md` but no `index.html`. It isn't Docsify.\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that runs `docsify serve <dir>` (usually the repo root), so the default `content.root: \"docs\"` matches. With no manifest, scaffold one there.\n\nRead the whole `index.html`: the `$docsify` object (it can be a function), every `<script src>` (each is a plugin), the `<head>`, and the `<body>`. Open a few routes on the live site too. **A page whose include target is missing rendered blank in Docsify 4**, so some \"pages\" never worked.\n\n## Run the codemod first\n\nOn the Docsify folder, from the directory that will hold `blume.config.ts`, dry-run the bundled codemod, then apply it once, before any hand edits:\n\n```bash\ncurl -s https://api.github.com/emojis > emojis.json   # only if pages use :shortcodes:\nnode <skill>/scripts/docsify-codemod.mjs docs\nnode <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old \\\n  --group-folders --emoji emojis.json --site https://docs.example.com docs\n```\n\nIt's zero-dependency and idempotent, and reads `version`, `relativePath`, `routerMode`, `noEmoji`, `homepage`, `loadSidebar`, `loadNavbar`, and `coverpage` from `index.html` (override with `--version` and `--relative-path`). It converts:\n\n- `!>` → `:::warning`, `?>` → `:::tip`, and `> [!TYPE]` alerts → directives, leaving fenced and indented code alone;\n- docsify-tabs → `<Tabs>`;\n- `':include'` lines → `<include>`, resolved from the page's folder as Docsify did, even with a leading `/`. A `:fragment=` is copied to its own file where Docsify cut one: code includes, plus Markdown ones in v5. Docsify 4 showed a Markdown include whole, so it stays whole;\n- `:id=` → `[#id]`, `{docsify-ignore}` → `[!toc]`, and setext headings → ATX;\n- links resolved the way Docsify resolved them (hash links, `?id=`, and `--site` absolute links included) → relative file links, without an anchor that named the target's title H1 (that heading becomes `title`);\n- images → rebased for moved pages. Docsify read a leading-slash image from the page's folder too, where Blume would read it from `public/`;\n- link and image attribute strings → dropped;\n- relative raw HTML URLs and `':ignore'` links → root paths. An `':ignore'` link to a page's own file opened the raw Markdown, so it becomes a raw `<a href>` to the page's Markdown copy (`/<new route>.md`): a Markdown link naming a page's file, even root-relative, lands on the page itself;\n- emoji shortcodes → Unicode;\n- the H1 or sidebar label → `title`, plus `sidebar.label`, `seo.title`, and `hidden: true`, with later H1s demoted;\n- `README.md` → `index.md`, and pages that need MDX → `.mdx` with MDX-safe comments, `<br>`, void tags, and autolinks.\n\n`--group-folders` moves each page into a folder per sidebar group, writes each folder's `meta.ts` in sidebar order plus a root `meta.ts` that keeps the groups in sidebar order, and writes the old → new route table to `docsify-routes.json`. Leave the flag off to keep the files where they are. With `homepage: \"intro.md\"`, the table also maps `/intro` → `/`, since Docsify served that page at both.\n\n`--snapshot` records every page's headings with the ids Docsify gave them, plus `old-routes.txt`. The pin step (URLs below) needs it, and it only works on the first run, while the pages are untouched. On Zenroom it matched Docsify 4.13.1's rendered ids on every page that rendered.\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each. The codemod never invents a fix: a missing include stays as written, and a file that must move to `public/` is only listed.\n\n## Config: `window.$docsify` → `blume.config.ts`\n\n| Docsify | Blume |\n| --- | --- |\n| `name` (may hold HTML) | `title`, as plain text. The `<title>` and `<meta name=\"description\">` in `index.html` give `title` (when `name` is unset) and `description` |\n| `logo` | `logo`, with the file moved to `public/`. Docsify shows the logo instead of the name, so a wordmark needs `logo: { image, text: \"\" }`. Check it in dark mode |\n| `nameLink` | `logo.href` |\n| `repo` | `github: { owner, repo }` (with `branch` from an edit-on-github URL). A non-GitHub repo goes in `navigation.actions` |\n| `themeColor` (v4), `--theme-color` in a stylesheet (v5) | `theme.accent` |\n| A dark-only theme (`dark.css`, a themeable `*-dark.css`) | `theme.mode: \"dark\"`. Other themes map to nothing |\n| Fonts in the stylesheet or `<link>`s | `theme.fonts` |\n| `<link rel=\"icon\">` | Copy the file to `public/favicon.<ext>` |\n| `<meta>` verification tags | `seo.metatags`. `twitter:site` goes to `seo.x.handle`. Drop `og:*` and `twitter:` card tags, since Blume writes them |\n| Custom CSS | A root `theme.css` with only the rules whose markup still exists. Docsify selectors (`.markdown-section`, `.sidebar`, `.cover`) match nothing |\n| Site URL (`CNAME`, a sitemap) | `deployment.site`. GitHub Pages isn't auto-detected |\n| `subMaxLevel: N` | `toc: { maxHeadingLevel: N }` when N ≥ 2 and isn't 3. Drop `maxLevel` |\n| `homepage: \"x.md\"` | Handled by the codemod. A remote URL needs vendoring |\n| `alias` | Local-to-local entries go in the route table. Remote targets: see Includes and aliases. Drop sidebar plumbing (`'/.*/_sidebar.md'`) and any alias whose target doesn't exist |\n| `externalLinkTarget: '_blank'` (explicit) | `markdown: { externalLinks: true }`. Docsify opens external links in a new tab even when this is unset, so report the change |\n| `notFoundPage` / `_404.md` | Blume's 404 page, or a custom `pages/404.astro` |\n| `formatUpdated`, `{docsify-updated}`, a last-modified plugin | `lastModified: \"git\"` (and `fetch-depth: 0` in CI) |\n| `collapsibleSidebarGroups` (v5) | `navigation.sidebar.display: \"group\"`, plus `collapsed: false` in each `meta.ts` that should start open |\n| `vueComponents`, `vueMounts`, `vueGlobalOptions`, `executeScript` | An island per widget (`islands/Name.vue`, `docs/content/islands.mdx`). Report |\n| `markdown` (marked options or a `renderer`) | Drop, and report what the renderer did. Common ones are mermaid (now native) and a `json` pretty-printer: minified JSON includes now render as one line, so fix that in whatever generates them. `breaks: true` has no equivalent |\n| `routes` (virtual routes) | Static ones become pages. Report dynamic ones |\n| `auto2top`, `topMargin`, `maxLevel`, `cornerExternalLinkTarget`, `requestHeaders`, `noCompileLinks`, `sidebarPosition`, `keyBindings`, `skipLink`, the search and pagination options, … | Drop |\n\n**Plugins** are `<script>` tags:\n\n- **Built in:** search, `docsify-copy-code`, `docsify-pagination`, and `zoom-image`. Delete them.\n- **Converted by the codemod:** emoji, `docsify-tabs`, and flexible-alerts.\n- **Prism language packs:** delete them. Shiki highlights the common languages in any case (` ```JSON ` works), and a fence in a language it doesn't know, or a misspelled one, renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: fix the name, or write `text`.\n- **Mermaid** plugins or renderers → native ` ```mermaid ` in `.mdx`.\n- **KaTeX** → `$$…$$` in `.mdx`, with inline `$…$` becoming `$$…$$` inside its sentence.\n- **`ga` / `gtag`** → `googleAnalytics({ id })` for a `G-` id. A `UA-` id no longer collects data, so report it. Other analytics → `script()`.\n- **Comments** (Disqus, Gitalk, giscus) → nothing built in. Use a `PageFooter` slot, or report them.\n- **Anything else** (bibtex, remote-markdown, demo boxes, custom inline plugins) → report.\n\n## Navigation: `_sidebar.md` → folders and `meta.ts`\n\n- **Groups over pages in one flat folder** (everything in `docs/` or `docs/pages/`) flatten silently. Use `--group-folders`: it moves each group's pages into a folder and writes `meta.ts` and the route table, and the hash script maps every old route. This departs from SKILL.md's `(group)/`-first rule: every Docsify URL changes form anyway (`/#/x` → `/x`), so the script runs for each old link either way, and the codemod rebases the moved pages' links and images. To keep URLs instead, move pages at the content root into `(group)/` folders, which add no URL segment; under a subfolder like `pages/`, they keep `/pages/x` but nest every group under a \"Pages\" group. Or keep the files and write an explicit `navigation.sidebar`.\n- **Groups whose pages already sit in a folder named for the group** (`Guide` → `guide/`) stay put, and `--group-folders` writes that folder's `meta.ts`. It moves the pages of a group whose folder has another name into one named for the group, so on a site already laid out in folders, leave the flag off and write each `meta.ts` by hand, with the group label and the sidebar's order.\n- **The home page** stays the root `index`. The generated sidebar lists it first, never inside a group.\n- **Loose pages listed after groups** can't keep that order, because Blume lists loose pages first. Move them into a folder, or report the change.\n- **External links, and links to files** like a PDF or HTML in `public/`, become `navigation.featured`. A file link there warns at build (\"no page matches it\"); the warning is harmless.\n- **A `_sidebar.md` inside a folder** replaced the sidebar under that folder. Map the folder to a `navigation.tabs` entry.\n- **Pages the sidebar doesn't list** get `hidden: true` from the codemod. They stay reachable, as before.\n- **Sidebar link titles** (`[x](x.md \"Tab title\")`) become `seo.title`. Report any that look copy-pasted from another page: Docsify showed them as the browser tab title.\n\n## Navbar and cover page\n\n- **`_navbar.md`:** doc-section links become `navigation.tabs` (a nested list becomes a tab with `items`), and other links become `navigation.actions`. With `mergeNavbar: true`, put the other links in `navigation.featured` instead, not in both: featured shows on every screen size, while actions hide on phones. Language links (`/zh-cn/`) become `i18n`.\n- **`_coverpage.md`** (shown above `README.md` on `/`; the whole home page with `onlyCover: true`): rebuild it as the top of `index.md`. The `# name <small>1.2</small>` heading is already the title. The tagline is prose or `description`, the logo is a Markdown image, and the buttons become links or `<Card>`s. Drop the background image or `![color](#…)`, and report it. Use `mode: center` when the cover was the whole page.\n\n## Content: what the codemod leaves to you\n\n**Titles.** Docsify drops only the first H1 from its sidebar headings, so pages often have several. The codemod takes a leading H1 as the title and demotes every later heading from the second H1 on. When content comes before the first H1, it takes the sidebar label as the title and demotes everything. Check every title it flags: about one page in five on Zenroom started with a section (\"Key generation\", \"Intro\"). For those, use the sidebar label as the title, put the H1 back as an `##` section, and demote every heading.\n\n**MDX hazards** in a renamed page. The codemod lists each one, and all but the first fail the build:\n\n- An indented code block renders as a plain paragraph: fence it.\n- An HTML table whose cell tags share a line with text: give each `<tr>`/`<td>` its own line.\n- A block element inside `<p>`, typical of README banners: make the `<p>` a `<div>`.\n- A repeated attribute: keep one.\n- A brace in prose: escape it as `\\{`.\n\n**Includes and aliases.**\n\n- **A missing include target:** the page was blank on the old site. Repoint the include to the file's new name (a folder was usually renamed), or drop it, and report it.\n- **A fragment copied from a generated file** goes stale. Have the generator write one file per fragment.\n- **An included file's own H1** becomes a second `<h1>`: remove it.\n- **An alias to a raw URL of a file in the same repo** (`'/CHANGELOG': 'https://raw.githubusercontent.com/<owner>/<repo>/main/CHANGELOG.md'`) becomes a page that `<include>`s the file through a symlink in `_snippets/`. Removing that file's H1 changes a file people read on GitHub, so say so.\n- **Other remote aliases and includes:** vendor a copy (with a source comment), or use `mdxRemote()`.\n- **iframe, video, and audio embeds** become raw HTML, with the file in `public/`.\n\n**Files the pages link** (listed under \"must be served from public/\"): Docsify served every file in its folder at its path. Blume publishes a file a page links relatively with that page, but at a new URL, and the codemod writes Docsify's root-resolved links as root paths, which keep the old URLs and are served from `public/`.\n\n- Copy each file into `public/` at the same path.\n- If a generator writes the files (test fixtures, examples), symlink the folder instead: `public/_media/examples → ../docs/examples`. Astro copies the target, and `validate` checks the links.\n- An HTML document saved as `.md` (generated API docs) goes to `public/` as HTML.\n- `blume validate --strict` reports a missing one as `BLUME_BROKEN_ASSET`.\n\n**Everything else:**\n\n- **Link attributes** the codemod dropped (`:target`, `:size`, `:no-zoom`, `:class`): restore any that matter as raw HTML (`<img src=\"/x.png\" width=\"200\" data-no-zoom />`, with the file in `public/`).\n- **Emoji shortcodes left over:** paste the emoji.\n- **Page `<script>`s:** Docsify 4 ran them only with `executeScript` or Vue, so they often never ran. Move one that did into an island.\n- **Vendored READMEs** keep Blume's ids (they had GitHub-style ids already), and they aren't in the snapshot.\n\n## URLs and redirects\n\n**Hash mode (the default).** Every page lived at `/#/<route>`, also reachable as `/#/<route>.md` and `/index.html#/<route>`, with folder indexes at `#/<folder>/` and headings at `?id=<id>`. The server only saw `/`.\n\n**A. Blume takes over the old address** (the usual case). `docsify-routes.json` holds the page moves the codemod made. Add an entry for each alias you turned into a page. Routes that now land on a file in `public/` go in a separate `files` map that only the script reads: a redirect page would sit where the file is. Name a `public/` folder with its slash (`/api/`) or name the file itself (`/api/index.html`): static hosts, `blume dev`, and `blume preview` serve its `index.html` at `/api/` (and redirect `/api` there), where its relative links resolve.\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport moved from \"./docsify-routes.json\" with { type: \"json\" };\n\n// Old routes that now land on a file in public/: the script sends readers\n// there, but a redirect can't, so they stay out of `redirects`.\nconst files = {}; // e.g. { \"/onepage\": \"/onepage.md\" }\n\n// Docsify served every page at /#/<route>; a server never sees the hash.\n// Swap an old hash URL for its new route, and ?id=<heading> for #<heading>.\nconst base = \"\"; // the site's deployment.base plus basePath, if it has either\nconst docsifyRedirects = `(() => {\n  const base = ${JSON.stringify(base)};\n  const routes = ${JSON.stringify({ ...moved, ...files })};\n  const go = () => {\n    const hash = location.hash;\n    if (!hash.startsWith(\"#/\")) return;\n    const q = hash.indexOf(\"?\");\n    let path = q === -1 ? hash.slice(1) : hash.slice(1, q);\n    const id = q === -1 ? null : new URLSearchParams(hash.slice(q + 1)).get(\"id\");\n    try { path = decodeURI(new URL(path, \"http://x\").pathname); } catch {}\n    path = path.replace(/\\\\.md$/i, \"\").replace(/(^|\\\\/)README$/i, \"$1\");\n    if (path.length > 1) path = path.replace(/\\\\/+$/, \"\");\n    // location.origin keeps a crafted #//other.host/ link on this site.\n    location.replace(location.origin + base + (routes[path] ?? path) + (id ? \"#\" + id : \"\"));\n  };\n  go();\n  addEventListener(\"hashchange\", go);\n})();`;\n\nexport default defineConfig({\n  redirects: Object.entries(moved).map(([from, to]) => ({ from, to })),\n  integrations: [\n    {\n      name: \"docsify-hash-redirects\",\n      hooks: {\n        \"astro:config:setup\": ({ injectScript }) => {\n          injectScript(\"head-inline\", docsifyRedirects);\n        },\n      },\n    },\n  ],\n});\n```\n\nThe script runs from every page's `<head>`, the 404 page included, before the body loads, in `blume dev` and in every build. Keep its two safeguards: `new URL(…).pathname` resolves the `./` and `../` segments Docsify left in links (`#/./guide`), and `location.origin` stops a crafted `#//other.host/` from sending readers off the site. Unmoved routes need no entry. The `redirects` also cover direct visits to a moved path and its `/old.md` Markdown twin. Don't use the `script()` analytics adapter for this: it's emitted in production builds only, and held until consent when `consent` is set.\n\n**B. The old site keeps its own address** (an old GitHub Pages URL, while the docs move to a new domain). Replace the old `index.html` with a page running the same parsing, with the new origin in place of `location.origin`, and a `<noscript><meta http-equiv=\"refresh\" content=\"0; url=https://new.example/\"></noscript>` fallback.\n\n**History mode** (`routerMode: 'history'`). URLs were real paths, so the route table and `redirects` carry moved pages. Anchors were still `?id=`, so add this after `go();`:\n\n```js\nif (!location.hash) {\n  const id = new URLSearchParams(location.search).get(\"id\");\n  if (id) location.replace(location.pathname + \"#\" + id);\n}\n```\n\n**Keep old heading anchors.** Docsify 4 leaks entity names into ids (`Foo & Bar` → `foo-amp-bar`, `Don't` → `don39t`), collapses dash runs, keeps emoji and shortcode names, and prefixes a leading digit with `_`. Docsify 5 is closer to Blume, but still differs in that prefix, in non-ASCII capitals, and where a heading has emphasis (`_When_` stays in the id) or a shortcode. After the first build, pin each old id where it differs, and rebuild until it reports 0:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json --write\n```\n\nOn Zenroom, one pass kept every old id except those of title H1s (58 of 968), and a link to one of those lands at the top of the page anyway. A page that never rendered on the old site (a missing include) has nothing to pin.\n\n**Check every old route** against `blume preview`. In `.docsify-old/old-routes.txt`, a folder route is listed without its slash, but Docsify served it only with one. Both forms reach the script.\n\n```bash\nnode -e '\nconst routes = { ...require(\"./docsify-routes.json\") };   // add the files map too\nconst old = require(\"fs\").readFileSync(\".docsify-old/old-routes.txt\", \"utf8\").split(\"\\n\").filter(Boolean);\n(async () => { for (const r of old) {\n  const res = await fetch(\"http://localhost:4321\" + (routes[r] ?? r));\n  if (res.status !== 200) console.log(res.status, r);\n} })();'\n```\n\nThen open a few old hash URLs, with and without `?id=`, in a browser on the deployed site. A plain `curl` can't test the hash script.\n\n**GitHub Pages**, where most Docsify sites live:\n\n- Switch the Pages source from the branch folder to GitHub Actions, using the workflow in https://useblume.dev/guides/markdown-docs-github-pages (with `fetch-depth: 0` for `lastModified: \"git\"`).\n- Pages has no redirect rules of its own. A static build writes each exact `redirects` entry as a page that forwards to the new URL, which is all the table holds; a pattern redirect wouldn't work there.\n- A project URL (`user.github.io/repo/`) becomes `deployment: { site: \"https://user.github.io\", base: \"/repo\" }`, and that base goes in the script too. Blume adds the base to root paths in Markdown and raw HTML alike, so the codemod's root paths need nothing more.\n- The custom domain stays in the Pages settings. Neither `CNAME` nor `.nojekyll` is needed.\n- The workflow's `npm ci` needs a lockfile, so commit one.\n\n## Teardown\n\nAfter harvesting what they declare, delete:\n\n- `index.html`;\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `_404.md` (Blume ignores `_` files meanwhile), or the files `loadSidebar`, `loadNavbar`, and `coverpage` name instead. Without the `_`, Blume publishes them as pages until they're gone;\n- `.nojekyll`, the theme CSS, and generators that read the sidebar or Docsify syntax (sitemaps, one-page builds).\n\nSwap `docsify-cli`/`docsify` for `blume` and `docsify serve` for `blume dev`/`blume build`/`blume preview`. Keep `docsify-routes.json` and `.docsify-old/` until the pins are done and old links stop mattering.\n\n## Dropped — report these\n\n- The cover's background and the navbar's language links (now `i18n`).\n- The home page's place in a sidebar group, and loose pages after groups.\n- Link and image attributes you didn't restore.\n- Fragments now frozen as copies.\n- Vendored or `mdxRemote()` aliases.\n- Pages that were blank on the old site (missing includes) and what you did with each.\n- Title H1 anchors (they land at the page top).\n- Comment plugins, a `UA-` analytics id, Vue widgets, and page scripts.\n- Custom `markdown` renderers (JSON pretty-printing included) and `breaks: true`.\n- Dynamic virtual `routes`.\n- Theme CSS with no Blume target, and copy-pasted sidebar link titles now in `seo.title`.\n- New-tab external links, unless you set `markdown.externalLinks`.\n",".agents/skills/blume-migrate/references/docus.md":"# Docus → Blume\n\nDocus is a Nuxt layer for docs sites. It's built on Nuxt Content (pages are Markdown files in `content/`, routed by their path) and Nuxt UI (the theme, and the prose components pages use). Navigation is the file tree plus per-folder `.navigation.yml` files, and settings live in `app.config.ts` and `nuxt.config.ts`. Pages are written in **MDC**, Nuxt Content's Markdown-with-components syntax. The bundled codemod converts most of it; these traps are why it exists:\n\n- **MDC's `::note` … `::` looks like a Blume directive and isn't one.** Blume prints `::note` and its `::` closer as text (in `.md`, every MDC construct), and `blume check` and `build` warn `BLUME_MDC_SYNTAX` at each block opener and inline component (`:badge[New]{color=\"primary\"}`) left in either format: convert them as below.\n- **MDX is stricter than MDC.** `[text]{.class}` spans fail the build, and so do `{…}`, `<Name>`, and `<50` in prose. Prompt bodies are full of these.\n- **Icons are Iconify names** (`i-lucide-zap`, `i-simple-icons-github`). Blume renders nothing for them, and warns only for sidebar and tab icons.\n- **Docus sorts siblings by file path as text,** so `10.faq.md` lists between `1.` and `2.`. Blume sorts the prefixes as numbers.\n\nScope:\n\n- **Docus 3.x–5.x** is covered: the `docus` package on Nuxt Content 3 and Nuxt UI, whose content syntax is the same across the three. 5.x is current and is by far the most installed, and it's the one this reference was run on.\n- **Docus 1.x** (`@nuxt-themes/docus`, on Nuxt Content 2 with Nuxt Elements components) is flagged. Its section near the end was written from the package source, not tested on a site, and the codemod doesn't convert its components.\n- **The Nuxt UI docs template** (`nuxt-ui-templates/docs`, no `docus` dependency) writes the same content: the codemod and this reference apply, and its `app.config.ts` is read by hand.\n\n## Detect\n\n- **A `docus` dependency** means 3.x–5.x, and **`@nuxt-themes/docus`** means 1.x. It's usually in the docs package's own `package.json` (`docs/`, `apps/docs/`) in a module's monorepo.\n- Usually a `nuxt.config.ts` with `extends: ['docus']`. The 4.x starters have no config and run `nuxt dev --extends docus`, and 3.x ran `docus dev`.\n- A `content/` folder beside it with `.navigation.yml` files (3.x+) or `_dir.yml` files (1.x).\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that depends on `docus`, and set `content.root: \"content\"`. `public/` stays where it is: both tools serve it at the site root. In a workspace, read `references/monorepo.md` too.\n\n- **No `content/docs/` folder:** every file under `content/` is a page, and `content/index.md` is the landing page at `/`.\n- **A `content/docs/` folder:** Docus publishes only `content/docs/**`, under `/docs/…`, plus `content/index.md`. Exclude anything else under `content/` and report it. Docus hides the `docs` level in its sidebar. In Blume, add a tab `{ label: \"Docs\", path: \"/docs\" }`, or the section tabs below, or the sidebar wraps everything in a \"Docs\" group.\n- **`content/<locale>/` folders:** see i18n.\n\n## Record the old site first\n\nBefore editing anything, save the URL list, the old heading ids, and the record of what rendered:\n\n- **URLs:** the live `/sitemap.xml` (on an i18n site, an index of one sitemap per locale), plus every `routeRules` redirect source in `nuxt.config.ts`.\n- **HTML:** `nuxt generate` writes `.output/public`, one `<route>.html` per page plus a meta-refresh stub per redirect. Copy it outside the repo; it's `<old build>` below. Or point the anchor script at the live origin.\n- **Check components against the live site.** A local build can miss a component a Nuxt module registers in its own build: it shows as an empty tag (`<InstallButton …></InstallButton>`) while production renders it. Request live pages with a browser user agent (`curl -A 'Mozilla/5.0'`): some Docus deployments answer `curl` with Markdown.\n\n## Run the codemod first\n\nOn the `content/` directory, dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/docus-codemod.mjs content          # report only\nnode <skill>/scripts/docus-codemod.mjs --write content  # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent. Icon names are checked against Blume's Lucide set once `blume` is installed (or with `--lucide <icons.json>`).\n\n**What it does:**\n\n- **Pairs MDC blocks** as remark-mdc does (see MDC syntax).\n- **Converts the Nuxt UI prose components:**\n  - callouts → `:::type` directives, or `<Callout type icon>` when a callout's icon isn't its type's own\n  - `card-group`/`card`, `field-group`/`field` → `<ResponseField>`, `steps`, `tabs`, `accordion`, `collapsible`\n  - `code-group` → `<CodeGroup>`, or one `package-install` fence when the group only repeats an install command\n  - `code-collapse`, `code-tree`, `code-preview`, `prompt`, `badge`\n  - the inline `:icon`, `:kbd`, `:badge`, `:video`, and `:u-color-mode-image`\n- **Strips Markdown attributes:** `[text]{.class}` spans and `{…}` attributes go.\n- **Rewrites fence labels and frontmatter:** fence `[label]`s become titles, `navigation` → `sidebar`, `seo.ogImage` → `seo.image`, `links` → `related`, a body H1 → `title`, and `.navigation.yml` → `meta.ts`.\n- **Renames to `.mdx`** each page that now needs MDX, and makes its prose MDX-safe (see MDC syntax).\n\nEvery `REVIEW` line is your to-do list, including:\n\n- custom and landing-page components, left as written\n- blocks with a slot the conversion doesn't read (a card's `#footer`), left as written\n- card descriptions, which Nuxt UI's prose card never rendered\n- callouts that linked somewhere\n- icons outside Lucide\n- unclosed blocks and stray closers\n- `links` lists longer than `related` takes, and frontmatter keys Blume rejects\n- MDC blocks inside blockquotes\n- file names whose URL changes\n\n## Config: `app.config.ts` → `blume.config.ts`\n\nRead `app/app.config.ts` (`app.config.ts` at the project root on Nuxt 3), `nuxt.config.ts`, the CSS that `css` lists, and `.env.example`.\n\n| `app.config.ts` | Blume |\n| --- | --- |\n| `seo.title` | `title`. Docus falls back to `site.name` in `nuxt.config.ts`, then the `package.json` `name`, then the git repository's name, so carry the one the site showed |\n| `seo.description` | `description` (Docus falls back to the `package.json` `description`) |\n| `seo.titleTemplate` | drop. The default `%s - <site>` is Blume's; report a custom one |\n| `seo.schema` | drop (report). Blume writes its own JSON-LD |\n| `header.title` | `title`, when there's no logo |\n| `header.logo.{light,dark,alt}` | `logo: { image: { light, dark, alt }, text: \"\" }`. Docus shows the logo without the title. A monochrome SVG → `currentColor` and the string form (SKILL.md) |\n| `header.logo.display: 'wordmark'` | the `wordmark` files as the logo |\n| `socials` (Simple Icons slug → URL) | `footer.socials` for the platforms Blume lists (`twitter` → `x`). Others (`nuxt`, `npm`) → `footer.links`, or drop (report) |\n| `github` (`url`, `branch`, `rootDir`), or nothing | `github: { owner, repo, branch, dir }`. **Docus reads the git remote when `github` isn't set,** so edit links showed anyway: take `owner`/`repo` from the remote. `dir` = `rootDir`; `branch` only when it isn't `main`. `github: false` → leave it out |\n| `toc.bottom.links` | `navigation.featured` (`to` → `href`). Report the move |\n| `navigation.sub` | `navigation.tabs` (see Navigation). `'aside'` anchors have no Blume form; report |\n| `docus.locale` (not `en`) | a one-locale `i18n` block |\n| `docus.colorMode` | `theme.mode`. Blume always shows the toggle; report |\n| `assistant.faqQuestions` | `ai.assistant.suggestions` (`[{ label }]`; flatten categories) |\n| `ui.colors.primary` | `theme.accent`. Nuxt UI paints shade 500 in light mode and 400 in dark: a Blume preset of the same name, else `{ light, dark }` with those Tailwind values. **Not declared means Docus's emerald:** the site turns Blume's default color unless you offer `theme.accent: \"green\"`; report it either way |\n| `--ui-radius: 0` in CSS | `theme.radius: \"none\"` |\n| `toc.title`, `docus.shortcuts`, `search.fts`, other `assistant.*` and `ui.*` keys | drop (report the `ui` overrides) |\n\n| `nuxt.config.ts` | Blume |\n| --- | --- |\n| `site.url` / `NUXT_SITE_URL` | `deployment.site`, unless the host is Vercel or Netlify (SKILL.md) |\n| `routeRules` with `redirect` | `redirects` (see URLs and redirects) |\n| `@nuxtjs/i18n` and `i18n` | see i18n |\n| `llms.notes`, `llms.sections` | `agents.llmsTxt: { details }` as Markdown, if worth keeping; `llms.title`/`description` drop |\n| `mcp` | Docus serves an MCP server at `/mcp` (or `mcp.route`) unless `mcp: false` or `mcp.enabled: false`. To keep one: `agents.mcp: { enabled: true, name }` (and `route` when it isn't `/mcp`) plus a host adapter (server output). Blume's tools are fixed, so report the site's own tools and prompts in `server/mcp/` |\n| `docus.assistant` | Docus turns the assistant on when the build has `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`: check the live site for it. Keep it as `ai: { assistant: { enabled: true, provider: gateway({ model }) } }` (`gateway` from `blume/ai`; Docus's default model is `google/gemini-3-flash`) plus a host adapter, or drop it (report) |\n| `app.head`, `@vercel/analytics`, `@nuxtjs/plausible`, `@nuxt/scripts` analytics | the provider's factory from `blume/analytics` (`vercel()`, `plausible({ domain })`, `cloudflare({ token })`); other scripts → `script()`; `meta` → `seo.metatags`. `@vercel/speed-insights` has no adapter (report) |\n| `content.build.markdown` (`toc`, `highlight.theme`, plugins), `mdc.highlight.theme` | `toc` and `markdown.code.theme` only when they differ from the defaults; each remark/rehype plugin's syntax converted by hand (report) |\n| `css` | what still applies → a root `theme.css` against Blume's tokens |\n| `studio`, `agentDiscovery`, `ogImage`, `sitemap`, `robots`, `schemaOrg`, `image`, `icon`, `nitro`, `vite` | drop (report). Blume writes its own sitemap, robots, OG images, and JSON-LD |\n\n**A project `content.config.ts`** adds collections such as a blog or changelog. Those aren't docs pages: map a blog to `type: blog` and a changelog to `type: changelog` (or `githubReleases()`, SKILL.md Changelogs), and report anything else.\n\n**Theme overrides** (`AppHeader*.vue`, `AppFooter*.vue`, `Docs*.vue`) aren't content. Map what they add to config (`navigation.cta`, `navigation.actions`, `footer.links`, `banner`), but read their `v-if`s first: a button shown only on the landing page (`route.path === '/'`) belongs on the landing page, since Blume's `cta` shows site-wide.\n\n## Navigation\n\nThe codemod writes each `.navigation.yml` as a `meta.ts` (`title`, `icon`) and each page's `navigation` as `sidebar` (`title` → `label`, `icon`, `false` → `hidden`). What's left:\n\n- **A folder's `navigation: false`** hides it: set `sidebar.hidden: true` on its pages (the codemod reports it).\n- **A folder with an `index.md` but no `.navigation.yml`:** Docus labels it with the index page's title. Blume uses the humanized folder name, so set `meta.ts` `title` when they differ.\n- **Order:**\n  - Docus compares paths as text: `1.intro`, `10.faq`, `11.glossary`, `2.setup`. Blume sorts the numbers. In a folder with ten or more unpadded siblings, keep Blume's order (almost always what the author meant) and report the change.\n  - Docus also interleaves pages and subfolders. Blume lists a level's pages above its subfolders, so report a page that sorted after a folder.\n- **Display:** Docus draws every folder as an always-open group, which is Blume's default `flat` display.\n- **`navigation.sub`** (a header tab bar, or anchors in the sidebar) makes each top-level folder a section that scopes the sidebar. That's `navigation.tabs`: one `{ label, path, icon? }` per top-level folder with pages, with `label` and `icon` from its `.navigation.yml`. A section with no index page needs no `href`, since a Blume tab falls back to the section's first page, as Docus did.\n\n## Pages: frontmatter and titles\n\nThe codemod maps `navigation`, `links` (header buttons → `related` cards at the foot of the page), and `seo.ogImage`. It removes `layout: docs` and `sitemap`. What's left:\n\n- **No `description`:** when a page opens with a paragraph (after an optional `# H1`), Docus took it as the description, so it showed twice. Blume derives none. To keep the meta description without the repeat, set `seo.description` to that paragraph (optional; say which you did).\n- **`sitemap: false`** has no sitemap-only switch: add `seo: { noindex: true }` only when the page should also leave search engines.\n- **`toc: false`** → `mode: wide` (report), or drop. Another `layout` → a Blume `mode`.\n- **Other keys** Nuxt Content kept and Blume rejects: drop and report each one, or declare it with `frontmatter.extend` if a component you wrote reads it.\n- **Titles:** Docus renders frontmatter `title` in the page header, falling back to the first `# H1`, so a body H1 showed twice. The codemod moves a lone H1 into `title` and reports the rest.\n\n## MDC syntax\n\nThe codemod applies these rules; you need them for what it leaves. They're remark-mdc's behavior (checked against `remark-mdc@3.11.1`):\n\n- **Block component:** an opening line `::name`, optionally followed by `{attributes}` and nothing else, then Markdown, then a closing line of colons only.\n  - Props are inline (`{key=\"value\" flag .class #id}`) or a YAML block, from `---` right after the opener to the next `---`. A `:key='[…]'` holds JSON.\n  - Slots are `#name` lines inside the block.\n  - An inline component is `:name[text]{attrs}`; alone on a line, it renders as a block.\n- **Pairing:**\n  - Every opener nests, even at the same colon count.\n  - A closer closes the innermost open block **whose opener has exactly as many colons**, together with anything still open inside it. It must not be indented deeper than that opener. Any other colon line, or a `::` mid-line, is text.\n  - An unclosed block runs to the end of its parent, and a closer inside a code fence is code.\n  - Check the old HTML for a literal `::` to see what the live site already showed broken.\n- **MDX safety:** in a `.mdx` page, prose must escape `{` and `}` as `\\{` and `\\}`, and turn **any `<` followed by a non-space character** into `&lt;` (or `\\<`) unless it opens a real tag: `useFetch<User>()`, `<script setup>` written as text, `<50 ms`, `<= 5`. Case matters as in JSX: `<input>` is HTML, but `<Input>` is a component, so a generic like `useTool<Input>()` is text. HTML comments become `{/* … */}`, `<https://…>` autolinks become links, and `<br>` becomes `<br />`. Code spans and `$$` math stay as written.\n- **Code fences:** ` ```ts [file.ts]{2} ` → ` ```ts file.ts {2} ` (`title=\"Two words\"` for a label with spaces). Blume reads ` ```ts [file.ts]{2} ` and a multi-word title as written too, the range highlighting its lines, so this only tidies. Leave MDC samples inside fences alone: they're documentation.\n\n## What the codemod leaves\n\n- **Custom components:** `app/components/content/*.vue`, or a Nuxt module's. Read the SFC to see what it renders and which props and slots it reads, then convert it:\n  - to Markdown\n  - to a Blume component\n  - to a static `.astro` component registered in `components.ts`\n  - to the `.vue` file as an island in `islands/`, with `@astrojs/vue` and `vue` installed. `:items='[\"a\"]'` becomes `items={[\"a\"]}`, and named slots become props or children.\n\n  Report each one. A component that rendered nothing on the live site (an unknown tag) has nothing to convert.\n\n- **Callouts with `to`:** the whole callout was a link. Add the link to its text when it isn't there already.\n- **Card descriptions:** Nuxt UI's prose card renders neither a `description` prop nor a `#description` slot, so that text never showed. The codemod keeps it as the card's body and flags it: keep it, or delete it to match the old page.\n- **Blocks with a slot the conversion doesn't read** (a card's `#footer`, a tab's extra slot) stay as written: convert them by hand.\n- **`code-tree`** becomes a `<CodeGroup>` titled by file path. The tree view is gone; a `<FileTree>` before it can show the layout.\n- **`prompt`:** Blume has no `windsurf` or `claude` action, and no prompt icon (the codemod reports both).\n- **Prose about features that didn't carry over:** grep the content for the dropped MCP tool and prompt names, `docus`, `/.well-known/skills`, and `Studio`, and list every page that now says something false.\n\n## Landing page\n\n`content/index.md` renders full-width, with no sidebar, title, or table of contents. Its `title` and `description` feed only the meta tags. The codemod leaves its `u-page-*` and custom components as written.\n\n- **Markdown landing** → `index.mdx` with `mode: custom` (no title or sidebar rendered, so write the heading) and `sidebar.hidden: true` (Docus never listed it).\n- **A designed landing**, or `app/pages/index.vue` → `pages/index.astro` on `PageLayout`.\n\n| Nuxt UI | Blume, in the `mode: custom` page |\n| --- | --- |\n| `u-page-hero` (`title`, `description`, `links`, or slots) | `# Title`, a paragraph, and a line of Markdown links; the primary one can also be `navigation.cta` |\n| `u-page-section` | `## Title`, a paragraph, and its features as cards |\n| `u-page-feature`, `u-page-card` (`title`, `description`, `icon`, `to`) | `<Card title icon href>description</Card>` in `<CardGroup cols={3}>` |\n| `u-page-grid`, `u-page-columns` | `<CardGroup cols={3}>` |\n| `u-button`, `u-page-cta` | Markdown links; a heading and a paragraph |\n| `u-page-logos`, decorative `div`s (glows, gradients) | a row of images, or drop (report) |\n\n## Icons\n\nBlume resolves Lucide names only (`zap`, or `lucide:zap`); **anything else renders nothing.** The codemod converts `i-lucide-*` (aliases of renamed icons included) and reports the rest:\n\n- **Other sets** (`i-heroicons-*`, `i-ph-*`): the closest Lucide name, checked at [lucide.dev/icons](https://lucide.dev/icons).\n- **Brands** (`i-simple-icons-*`): Lucide has a few (`github`, `gitlab`, `slack`, `linkedin`, `youtube`), but **`i-simple-icons-x` is not Lucide's `x`, a close cross.** Otherwise, save the SVG under `public/` and pass its path (`icon=\"/icons/nuxt.svg\"`), or drop it (report).\n- **`i-custom-*`** (Docus's `app/assets/icons/*.svg`): copy the SVG to `public/icons/` and pass the path.\n\n## i18n\n\nDocus i18n is `@nuxtjs/i18n` (`defaultLocale`, `locales: [{ code, name }]`) plus one `content/<code>/` folder per locale, and it forces the `prefix` strategy: every locale is under its code, the default included, and `/` redirects to the default.\n\n- Move `content/<default>/` up to `content/` (a default-locale folder warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`). Keep the others as `content/<code>/`, and run the codemod on `content/`.\n- Write `i18n: { defaultLocale, locales: [{ code, label }], hideDefaultLocalePrefix: false }`, `label` = `name`. That keeps every `/en/…` URL. Nothing serves `/` then, so add `{ from: \"/\", to: \"/<default>\", status: 302 }` as Docus did.\n- **Regional codes are lowercase in Docus URLs** (`zh-TW` → `/zh-tw/…`): use the lowercase code as Blume's `code` and folder.\n- Leave out any configured locale with no `content/<code>/` folder: Docus skipped it.\n\n## URLs and redirects\n\nDocus routes a page by its path under `content/`. It strips an `N.` prefix from each segment, drops `index`, and slugifies (lowercase, spaces → `-`). Blume strips `N.`, `N-`, and `N_` prefixes and `index` the same way, so **most URLs carry over unchanged**, with no trailing slash or `.html` to redirect. The codemod reports the exceptions:\n\n- **A dash prefix** (`01-intro.md`) stays in the Docus URL and Blume strips it: pin `slug` or redirect.\n- **Uppercase letters or spaces** were lowercased by Docus: rename the file.\n- **A version name** (`1.x`, `2.0`) stays whole in Docus: pin `slug`.\n- **`_` files** are pages in Docus and excluded by Blume: rename them.\n\n**Redirects:**\n\n- **`routeRules` → `redirects`.** A string `redirect` is a 307 in Nitro (`status: 307`); `{ to, statusCode }` → `{ to, status }`. A `/**` key strips its base: `'/old/**': { redirect: '/new/**' }` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`.\n- **`/raw/<route>.md`** is the Markdown copy of each page, which Docus links from `llms.txt`. Blume serves it at `/<route>.md`, so one rule covers it: `{ from: \"/raw/:path*\", to: \"/:path*\" }`. A `routeRules` redirect between two `/raw/…md` paths should target Blume's copy directly (`/raw/old.md` → `/new.md`), or it chains through that rule.\n- **Agent skills:** a `skills/` folder Docus published under `/.well-known/skills/` → `agents.skills: \"./skills\"`. Blume publishes them under `/.well-known/agent-skills/`, a skill with references as one `.tar.gz`. Redirect the old index to the new one, and report the change. Links can name the new index (`/.well-known/agent-skills/index.json`): `blume validate` accepts the files Blume generates.\n- **Anchors:** MDC slugs headings like Blume (`github-slugger`), then collapses repeated dashes, trims edge dashes, and prefixes a leading digit with `_`. `## Name & Title` is `#name-title` in Docus and `#name--title` in Blume; `## 1. Install` is `#_1-install`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old <old build>`, `--write`, and rebuild until it reports 0. On a server build (`vercel()`, `node()`) it reads the pages from `dist/client` itself.\n- **Verify** every old URL: a built page, or a redirect `from` whose target is built (SKILL.md Verification).\n\n## Deploy and teardown\n\n- **Keeping the assistant or the MCP server** needs server output: `deployment: vercel()` (or `node()`, `netlify()`, `cloudflare()`). `blume preview` can't serve a `vercel()` build, so check it with `blume dev`, then a preview deployment.\n- **Vercel with `vercel()`:**\n  - The build writes the Build Output API folder `.vercel/output` in the Blume project, not `dist/`.\n  - In the project's `vercel.json`, set `\"framework\": null` and the install and build commands (`references/monorepo.md` §4 for a workspace), and **no `outputDirectory`**. `framework: null` overrides a project still set to the Nuxt preset.\n  - In the dashboard, set the Root Directory to the docs package and Node to 22.19 or later. Set `AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token.\n  - With Turborepo, add `.vercel/output/**` to the docs' build outputs.\n  - List the dashboard settings as steps for the user.\n- **Dependencies:** remove `docus`, `nuxt`, the `@nuxt/*`/`@nuxtjs/*` modules, `nuxt-studio`, `better-sqlite3`, `@iconify-json/*`, `tailwindcss` (if only the theme used it), mapped analytics packages, `@nuxt/eslint-config`, and `vue` (keep it with `@astrojs/vue` for islands). Keep what the site's tests or scripts still import.\n- **Scripts:** `dev`/`build`/`generate`/`preview` → `blume dev`/`build`/`build`/`preview`. **Delete `postinstall: nuxt prepare`**, which fails once Nuxt is gone.\n- **Files:** after harvesting, delete `nuxt.config.ts`, `app/`, `content.config.ts`, `server/` (report its routes), and a Nuxt `eslint.config.mjs`. The codemod leaves a `.navigation.yml` whose keys `meta.ts` can't hold.\n- **`tsconfig.json`:** a Nuxt one points into `.nuxt/`, and `blume build` then fails with `BLUME_TSCONFIG_EXTENDS`. Delete or replace it.\n- **`.gitignore`:** in the docs package's own file, `.nuxt`, `.output`, `.data` → `.blume`, `dist`.\n- **CI:** grep for `nuxt generate`, `nuxi`, `.output/public`, and `--extends docus`.\n\n## Docus 1.x (`@nuxt-themes/docus`)\n\nWritten from `@nuxt-themes/docus@1.15.1` and `@nuxt-themes/elements@0.9.5`. Everything above applies (run the codemod for fences, frontmatter, and the H1), with these differences:\n\n- **Config** is `app.config.ts` → `docus: { … }`:\n  - `title`, `description` → same keys\n  - `url` → `deployment.site`\n  - `socials` → `footer.socials`. Values are handles (`twitter: '@nuxt_js'`, `github: 'owner/repo'`), so build each URL.\n  - `github: { owner, repo, branch, dir }` → `github`\n  - `header.logo`/`title` → `logo`/`title`\n  - `aside.collapsed` → `navigation.sidebar.display: \"group\"`\n  - `footer.textLinks`/`iconLinks` → `footer.links`\n  - everything else → drop (report)\n- **Folders** use `_dir.yml` (`title`, `icon`, `navigation: false`, `navigation.redirect` → a redirect from the folder's route).\n- **Titles** are the body `# H1` (the codemod moves it).\n- **Frontmatter:**\n  - `layout: page` → `mode: custom` or `mode: center`\n  - `aside: false` → `mode: center`\n  - `toc: false` → `mode: wide`\n  - `redirect` → a redirect\n  - `bottom`, `header`, `fluid`, `main` → drop\n- **Components** (Nuxt Elements, left as written and reported):\n  - `::alert{type}` → that directive (`primary` → `tip`)\n  - `::callout{type}` with `#summary`/`#content` → the directive holding the summary, then `<Expandable title=\"Details\">` around the content\n  - `::list{type}` → a plain list\n  - `::badge{type}` → `<Badge>`\n  - `::button-link{href}` → a link\n  - `::terminal{content}` → a `bash` fence\n  - `::card-grid` + `::card{icon}` with `#title`/`#description` → `<CardGroup>` + `<Card>`\n  - `::block-hero` → the landing page\n  - `::code-block{label}` → a titled fence in `<CodeGroup>`\n  - `::video-player{src}` → `<video>`\n  - `::sandbox`, `::props`, `::copy-button`, `:ellipsis` → drop (report)\n- **URLs:** Nuxt Content 2 routes the same way, so check the old sitemap for trailing slashes and pin heading ids as above.\n\n## Dropped — report these\n\nReport each of these, and anything else you drop:\n\n- **Theme:**\n  - the brand color, when it was Docus's default emerald\n  - Nuxt UI overrides and theme CSS that doesn't map\n  - a custom `seo.titleTemplate`, `seo.schema`, and the TOC title\n  - logo extras, and socials with no Blume platform\n  - a forced color mode (the toggle stays)\n  - `navigation.sub: 'aside'` anchors\n  - a header button that showed only on the landing page (now site-wide)\n- **Navigation:** text-order sorting (`10.` before `2.`), and pages that sorted after a folder.\n- **Frontmatter:**\n  - `links` (now `related` cards)\n  - `sitemap: false`, `toc`, `layout`, and other unknown keys\n  - derived descriptions\n- **Components:**\n  - callout `to` links, callout colors (mapped to the nearest type; `<Callout color>` takes a CSS color when a tint matters), and card colors\n  - prompt icons and Windsurf/Claude actions\n  - the `code-tree` file browser and `code-preview` live rendering\n  - Nuxt UI landing decorations\n  - custom and module components\n  - brand icons with no SVG\n- **Server features:**\n  - the site's own MCP tools and prompts\n  - server routes and `@vercel/speed-insights`\n  - the assistant, if not kept\n  - old `/.well-known/skills/` file URLs, and pages whose prose describes any of these\n- **Anchors:** old ids that were already broken.\n",".agents/skills/blume-migrate/references/docusaurus.md":"# Docusaurus → Blume\n\nDocusaurus is a React site; navigation comes from `sidebars.js` (explicit or autogenerated) plus per-folder `_category_.json`. Admonitions are already directive syntax, which eases the content transform — **but note the source major version**: v2 (MDX v1) and v3 (MDX v3) have different content hazards, called out below.\n\n## Detect\n\n- **`docusaurus.config.{js,ts}`** at the root; a **`sidebars.{js,ts}`** file and per-folder **`_category_.json`** files.\n- Content under **`docs/`** (often plus `blog/`, `src/pages/`, `static/`, `versioned_docs/`); `@docusaurus/*` deps.\n- v2 vs v3: check the `@docusaurus/core` major in `package.json`.\n\n## Config: `docusaurus.config` → `blume.config.ts`\n\nRead `themeConfig`, `presets`, and `plugins`:\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` | `title` |\n| `tagline` | `description` |\n| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |\n| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |\n| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead (it renders in the footer) |\n| `themeConfig.navbar.items` (a `className`-styled button — \"Get started\", \"Sign up\") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |\n| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `\"system\"`) |\n| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |\n| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |\n| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md — GitHub Pages, a common Docusaurus host, is not one of them); `baseUrl` (when not `/`) → `deployment: { base: \"/…\" }`, or the `base` option of a host adapter (`vercel({ base })`) when the site also needs one |\n| preset `docs.routeBasePath` — **including the default!** | Docusaurus serves docs at **`/docs/…` by default**; the \"map only declared fields\" rule does **not** apply here because the _URLs_ are load-bearing. Either keep them with top-level **`basePath: \"/docs\"`** (invisible to the sidebar), or intentionally move to root and emit a `redirects` entry per page. Decide explicitly and say which. (`routeBasePath: '/'` = docs-only mode — nothing to do.) |\n| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`; **an origin other than `https://github.com` → `github.host`** — a GitHub Enterprise repo's edit links and header mark point at the public site without it) |\n| `themeConfig.footer` | `footer.links`, one row of `{ label, href }`: every column's items flatten into it in order (an item's `to`, a route, is its `href`); `copyright` (HTML) → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), keep a `${new Date().getFullYear()}` template literal as written, and drop a \"Built with Docusaurus.\" credit; a link inside it loses its URL (report, or add it to `footer.links`); a translated one in `i18n/<locale>/docusaurus-theme-classic/footer.json` → a locale map; column `title`s, `logo`, and `style` drop (report), or keep the whole footer as a Footer override (`defineComponents` layout slot) |\n| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |\n| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |\n| `themeConfig.algolia` | drop — Blume ships built-in search (Orama); remove the Algolia dep. To keep the index, `search: algolia({ appId, apiKey, indexName })` from `blume/search` (the DocSearch `apiKey` is the search-only key) |\n| `@docusaurus/plugin-client-redirects` | **static `redirects: [{from, to}]` arrays convert 1:1** to Blume `redirects` (a `from` array = one entry per item); only `createRedirects` functions are truly dynamic → host rules |\n| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |\n| OpenAPI doc plugins (`docusaurus-plugin-openapi-docs`, `redocusaurus`) | delete the plugin **and its generated pages** — add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list; `redocusaurus` `specs[].route` → the adapter's `route`, one `openapi()` entry (or source) per spec (see SKILL.md \"OpenAPI\") |\n| GraphQL doc generators (`@graphql-markdown/docusaurus`, `@edno/docusaurus2-graphql-doc-generator`) | delete the plugin **and its generated pages** — add `graphql({ spec, endpoint })` (from `blume/reference`) to the top-level `reference` list instead (see SKILL.md \"GraphQL\") |\n| `remark-math` + `rehype-katex` | delete — `$$…$$` renders in `.mdx` with no config (no `markdown.math` field exists), as a block or inline within a sentence; a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence |\n| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |\n\n## Static assets — required move\n\nEvery Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png` → `/img/foo.png`). Blume serves **`public/`**. **Move `static/*` (and any extra `staticDirectories`) into `public/`** — all root-relative references then keep resolving unchanged. Without this, every image and download 404s.\n\n## Navigation: `sidebars.js` + `_category_.json`\n\n- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.\n- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: \"group\"` (or `navigation.sidebar.display: \"group\"` once in config when every category collapses).\n- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder (its `title`/`description` from `link.title`/`link.description`) plus `directory: \"card\"` in the folder's `meta.ts`, which lists the folder's pages below it as the generated index did — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible: false` → that folder's `meta.ts` `display: \"flat\"`, so it stays a plain heading even under a global `\"group\"` mode. `className`/`customProps` → drop (report).\n- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.\n\n## Versioned docs\n\nDocusaurus `versioned_docs/version-X/` + `versions.json` (+ `versioned_sidebars/`) → **recommend migrating the latest released version only**. Mind the URL scheme: by default the **latest release** serves at `/docs/` and the work-in-progress `docs/` folder serves at `/docs/next` (`lastVersion: 'current'` flips this) — pick the folder that matches what users see at `/docs/`. If older versions must stay, use Blume's native versioning rather than a hand-built `navigation.selectors` dropdown: move each `versioned_docs/version-X/` into a top-level folder under `content.root` named for its id, and list that id in `versions.archived` (newest first), with `versions.current` labeling the live tree. Ids must start with a letter, so `version-1.0/` becomes `v1.0/`. Blume then adds the version switcher, the old-version notice, version-scoped search, and canonicals to the latest itself. A version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as ordinary current content. Snapshot routes become `/<id>/…`, so rewrite root-absolute links inside each snapshot to stay in it (`/guides/x` → `/v1.0/guides/x`) and add `redirects` from the old version URLs. Full reference: `docs/content/versioning.mdx` in the installed package.\n\n## Blog\n\nA Docusaurus `blog/` → Blume `type: blog` pages. **Dates come from filenames/folders** (`2024-01-31-foo.md`) — extract each into `date` frontmatter and strip the date from the filename (the old dated URLs `/blog/2024/01/31/foo` need `redirects`). Strip `<!-- truncate -->` / `{/* truncate */}` markers. `authors.yml` refs → inline author objects in each post's `authors` frontmatter. RSS stays at `/blog/rss.xml` on both sides. Blume generates **no** blog index, tag, author, or archive pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag, author, and archive pages as dropped.\n\n## Content & components\n\n- **`.md` vs `.mdx` — both majors need renames, for opposite reasons.** Blume parses `.md` as plain Markdown: no directives, no JSX, no `$$` math, no mermaid/package-install fences. **v3** treats `.md` as MDX (so a `.md` with imports/JSX/`{}` renders them as literal text in Blume); **v2** content is looser MDX v1. Rule: **rename any `.md` that contains admonitions, JSX, imports, or math to `.mdx`** — for typical Docusaurus repos that is most files.\n- **Admonitions are directives — but check the version.** v3: `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger` pass through; `:::caution` → `:::warning` (or rely on Blume's alias); titles `:::note[Title]` work. **v2:** titles are space-separated (`:::note Your Title`) — rewrite to brackets or the title is lost (`BLUME_DIRECTIVE_OPENING_TEXT` names each one); and v2's `:::warning` rendered **red/danger** — audit whether it should become `:::danger`.\n- **Tabs:** `<Tabs>`/`<TabItem label=\"…\" value=\"…\">` → `<Tabs>`/`<Tab title=\"…\">`. Drop `groupId`/`queryString`/`value`; strip the `@theme/Tabs` imports.\n- **Theme JSX in content:** `<DocCardList/>` (standard on category index pages) → delete it and set `directory: \"card\"` in that folder's `meta.ts`: Blume then lists the folder's pages as cards below the `index` page's content and keeps the list current (nested folders inherit the setting; give one `directory: \"none\"` to opt out). A hand-written `CardGroup` is only for a curated subset; `<TOCInline/>` → drop (report); `<CodeBlock>` JSX → a fenced code block; `<Admonition>` → the matching directive; `<details>`/`<summary>` → `<Accordion>`/`<AccordionItem>` or leave as raw HTML.\n- **`@theme/*` / `@site/*` imports** — strip `@theme/*` (Blume injects components globally); rewrite `@site/` asset/module paths to `/public` URLs or inline. **MDX partials** (`_partial.mdx` imports) → inline the partial's body (Blume's default `**/_*` exclude already hides the partial files themselves).\n- **Code blocks:** `title=\"file.js\"` → works as-is; `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left); **magic comments** (`// highlight-next-line`, `highlight-start`/`end`) → `{ranges}` or `// [!code highlight]` — unconverted they ship as literal comments in every sample; ` ```bash npm2yarn ` → ` ```package-install `.\n- **MDX v1 (v2 sources) pitfalls:** unescaped `<`/`{` in prose, HTML comments `<!-- -->` (→ `{/* */}`), string `style=\"…\"` attributes (→ objects). Fix as build errors surface.\n\n## Frontmatter\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` / `description` | pass through |\n| `id` | **not a safe drop** — Docusaurus builds a doc's URL from its ID, so `id: part1` in `guides/hello.md` serves `/docs/guides/part1`. Where the `id` differs from the filename (and no `slug` is set), replace it with `slug: guides/part1` (the folder path plus the id) or rename the file to match; otherwise drop it. Sidebar and `link.type: 'doc'` references name docs by this ID too |\n| `slug` | `slug` as a **full path from the content root**: Blume's `slug` replaces the page's whole route, while Docusaurus resolves a relative slug (no leading `/`) against the doc's folder. So `guides/intro.md` with `slug: start` → `slug: guides/start`; an absolute slug (`/start`) is already a full path |\n| `sidebar_label` | `sidebar.label` |\n| `sidebar_position` | `sidebar.order` |\n| `unlisted` | `hidden: true` + `noindex: true` |\n| `last_update.date` | `lastModified` |\n| `draft` | `draft` |\n| `tags` | `search.tags` (or drop if only used for the blog tag index) |\n| `image` | `seo.image` |\n| `keywords` | drop → use `seo` |\n| `sidebar_class_name` / `sidebar_custom_props` / `pagination_next` / `pagination_prev` / `pagination_label` / `hide_title` / `hide_table_of_contents` / `toc_min_heading_level` / `toc_max_heading_level` / `displayed_sidebar` / `custom_edit_url` | drop (report) |\n\nRemove any duplicated H1 in the body — `title` renders the H1.\n\n## Icons\n\nDocusaurus has no built-in icon-name system (icons are usually inline SVG/emoji/CSS). Where you add sidebar/tab icons, use Lucide names. Convert any FontAwesome/react-icons usage in content to Lucide `<Icon>` or drop it.\n\n## Teardown\n\nRemove `@docusaurus/*` and Algolia deps; delete `docusaurus.config.*`, `sidebars.*`, `babel.config.js`, and `src/` theme customizations (after the `src/pages/` inventory above; re-implement what matters via `defineComponents` layout slots or `blume eject`). Repoint `start`/`build`/`serve` scripts → `blume dev`/`blume build`/`blume preview`; add `blume`.\n\n## Dropped — report these\n\nCustom/swizzled theme components (layout slots or `blume eject`), the footer's logo and HTML items (and any link inside its copyright), the blog's generated tag, author, and archive pages, Algolia config, `sidebar_custom_props` and the other dropped frontmatter keys, `createRedirects` functions (→ host rules), React pages under `src/pages/`, `<TOCInline>`, per-category `className`/`customProps`, and any `@theme/*` component with no Blume equivalent.\n",".agents/skills/blume-migrate/references/fern.md":"# Fern Docs → Blume\n\nFern Docs is a hosted platform driven by a `fern/` folder: `docs.yml` declares the **entire** navigation (tabs, sections, pages, the API Reference, changelogs, versions, products), pages are MDX files anywhere under `fern/`, and the API Reference is generated from an OpenAPI/AsyncAPI spec or a **Fern Definition**. Two things set it apart from the other sources:\n\n- **The same `fern/` folder usually drives SDK generation too** (`generators.yml`, `definition/` or `apis/<name>/`). Only the docs move. Never delete or rewrite the API definition, a `generators.yml`, or `fern.config.json` (see [Teardown](#teardown-keep-sdk-generation)).\n- **URLs come from `docs.yml`, not from file paths.** A page's URL joins slugs from its tab, sections, and page name, and a frontmatter `slug` replaces most of that. Files sit wherever the team put them (`pages/get-started/welcome.mdx` can serve `/welcome`).\n\nScope: checked against Fern CLI 5.44.8 and an end-to-end migration of agentmail-to/agentmail-docs (115 pages, a Fern Definition with 149 endpoints and 8 webhooks, 270 old URLs). Versions, products, tab variants, translations, `folder:` entries, and OpenAPI (rather than Fern Definition) inputs weren't in that run: the guidance for them below is untested.\n\n## Detect\n\n- **`fern/docs.yml` beside `fern/fern.config.json`** (run from the repo root), or `docs.yml` + `fern.config.json` in the working directory (run from inside `fern/`). Monorepos nest it (`docs/fern/docs.yml`).\n- `fern.config.json` **without** a `docs.yml` is an SDK-only Fern project: the docs live elsewhere (often a Mintlify `docs.json`), so use that source's reference and leave `fern/` alone.\n- A stale `fern/docs.yml` can outlive a move off Fern. If the repo also carries another docs config, check which one the live site runs: Fern pages answer with an `x-fern-renderer` header (`curl -sI https://<docs-domain>/ | grep -i x-fern`).\n\n## Run the codemod first\n\nThe bundled codemod does the mechanical half: URL computation and file placement, `meta.ts`, frontmatter, the common components, assets, and every redirect. Steps, in order:\n\n1. **Save the old URL list.** Fetch `https://<docs-domain>/sitemap.xml` and keep only the paths, one per line, in `old-urls.txt` (a Fern sitemap can name a different host than the one you fetched). Hidden pages aren't in it; the codemod computes those from `docs.yml`. Run the CORS probe under [Try it](#layout-summary-snippets-try-it) while you're there.\n2. **Dry run**, from the folder that holds `fern/` (where `blume.config.ts` goes):\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt\n   ```\n\n   It prints a sitemap diff (`0 computed URL(s) not in it, 0 of its URLs not computed` is the goal; otherwise fix those pages by hand with [Fern's URL rules](#ferns-url-rules) and report them), the `navigation.tabs` entries for the config, which APIs are Fern Definitions, a TODO list of judgment calls, and the icons it couldn't map. `--out` sets the content root (default `docs`), `--public` the public folder, `--fern` the Fern folder. It **stops** on `versions` or `products`: migrate those by hand.\n\n3. **Apply** with `--write`. It moves every published page to a path that reproduces its old URL, writes frontmatter and `meta.ts` files, converts components, moves the assets pages use (and the logo and favicon) into `public/`, and saves `fern-migration.json`. A YAML or JSON file a page links to (a spec offered for download) is copied, not moved, since SDK generation may read it. It's idempotent: a rerun skips moved pages.\n4. **Write `blume.config.ts`** from [Config](#config-docsyml--blumeconfigts), with the printed tabs and `logo.href` set to the page `/` now redirects to.\n5. **Set up the API reference** ([API reference](#api-reference)): export a Fern Definition, write the overlay (tag names, webhooks, idempotency headers), add `openapi()`.\n6. **Build, then list the operation routes**:\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist > routes.json\n   npx fern-api@<version> export --api <api-name> spec.json   # JSON because the path ends in .json\n   ```\n\n   The join needs a JSON copy of the spec the build rendered: for a Fern Definition, that export (a scratch file, not the committed spec); for a YAML OpenAPI input, `npx @redocly/cli bundle <spec>.yaml --output spec.json`.\n\n7. **Join endpoints and write the redirects**:\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt          # dry run\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt --write\n   ```\n\n   One `--spec <route>=<file>` per API. It maps every old endpoint, webhook, and group URL to its Blume route, translates `docs.yml` redirects, adds the content-move, changelog, feed, and `/` redirects, rewrites content links that point at old URLs, and checks every old URL is served or redirected (it must print `every old URL is served or redirected`). `--write` saves `fern-redirects.json`; import it:\n\n   ```ts\n   import fernRedirects from \"./fern-redirects.json\" with { type: \"json\" };\n   // in defineConfig:\n   redirects: [...fernRedirects],\n   ```\n\n   Fix a redirect it flagged by filtering that entry out and adding your own after the spread.\n\n8. **Work the TODO list**, then `blume build`, `blume validate --strict`, `blume audit --only redirects`, SKILL.md's heading-anchor check (`pin-heading-ids.mjs`), and `fern check` after the [teardown](#teardown-keep-sdk-generation).\n\nWhat it leaves to you: the config, the overlay, the TODO list, the components it names, links already broken on Fern, CI, and contributor docs. It never touches `generators.yml`, the definition, or a spec.\n\n## Inventory\n\nRead `docs.yml` first, then every file it points at: `redirects` (a list, or YAML files that each hold a `redirects:` list), `tabs.<key>.changelog`, every `path:` in `navigation`, `css`, `js`. **Every path in a Fern YAML file is relative to that YAML file.**\n\n- **Unpublished drafts.** Fern builds only the files `docs.yml` references plus each changelog folder; any other `.md`/`.mdx` under `fern/` is a draft nobody sees. The codemod leaves them in place and lists them. Blume would publish them: delete them or move them to a `_`-prefixed folder (`_drafts/`), and report the list.\n- **SDK generation.** Every `generators.yml` and its `groups` stay exactly as they are.\n- **Second instances.** Each `instances[]` entry is a deployment of the same docs (a second domain, a subpath). One Blume build serves one; report the rest (see [Deploy](#urls-redirects-and-deploy)).\n\n**Where the Blume project goes.** If the repo root has a `package.json`, put `blume.config.ts` there with content in `docs/` (the default root). If it has no JavaScript toolchain (a Python or Go repo whose docs were only `fern/`), make a self-contained docs package (`docs/` holding `package.json`, `blume.config.ts`, and `content.root: \"content\"`), run the codemod from there with `--fern ../fern --out content`, and say which you chose. Don't make `fern/` itself the content root: it keeps holding the SDK config. See `references/monorepo.md` for workspaces.\n\n## Config: `docs.yml` → `blume.config.ts`\n\nMap only what's set.\n\n| Fern `docs.yml` | Blume | Notes |\n| --- | --- | --- |\n| `title` | `title` | Fern shows it only in the browser tab; Blume also shows it beside the logo (see `logo`) |\n| `logo` (`{ light, dark, href, height, right-text }`) | `logo: { image: { light, dark, alt }, href, text }` | the codemod moves the files into `public/`. Fern shows the image alone unless `right-text` is set, so set **`text: right-text`, or `text: \"\"`**. A local monochrome SVG can collapse to `logo: \"/logo.svg\"` with `currentColor` fills (SKILL.md). Set `href` to the first page when nothing serves `/`. `height` drops |\n| `favicon` | **drop the field** | the codemod moves the file to `public/favicon.<ext>`; Blume detects it by filename |\n| `colors.accent-primary` (or `accentPrimary`), a string or `{ light, dark }` | `theme.accent` | same shape |\n| `colors.background` | `theme.background` | same shape |\n| `colors.*`, `layout.*`, `settings.*` (others), `theme.*`, `global-theme`, `page-actions`, `experimental`, `check`, `ai-examples`, `libraries`, `roles` | **drop** (report) | `global-theme` merged a registry theme at publish: read its values off the live site |\n| `typography.bodyFont` / `headingsFont` / `codeFont` | `theme.fonts.body` / `display` / `mono` | Fern fonts are files in the repo (`path`, or `paths` with `weight`/`style`): each role takes `{ name, variants: [{ src, weight, style }] }` |\n| `navbar-links` `type: filled` / `primary` | `navigation.cta` (`{ label: text, href }`) | one only; extra filled links go to `actions`. `url` is Fern's old name for `href` |\n| `navbar-links` `type: minimal` / `outlined` / `secondary` | `navigation.actions` (`[{ label: text, href }]`) | text only: link icons drop. A `mailto:` link works |\n| `navbar-links` `type: github` / `dropdown` | `github: { owner, repo }` / **no equivalent** | flatten a dropdown into `actions` (report) |\n| `footer-links` | `footer.socials` | keys carry over except `twitter` → `x`, `hackernews` → `hacker-news` |\n| `footer` / `header` (React files) | **drop** (report) | or a layout slot in `components.ts` |\n| `instances[].custom-domain` | `deployment.site` (`https://…`) | only when the target host isn't Vercel or Netlify (SKILL.md). **A domain with a path** (`acme.com/docs`) is a reverse-proxied subpath → `deployment: { base: \"/docs\" }`. **Several instances** → one Blume build; keep the canonical one (`metadata.canonical-host` names it when set) and report the rest |\n| `instances[].edit-this-page.github` (`{ owner, repo, branch }`) | `github: { owner, repo, branch, dir }` | `dir` = path from the repo root to the folder holding `blume.config.ts` |\n| `metadata.canonical-host` | `deployment.site` | same host rule as `custom-domain`; a path in it is the `base` above |\n| `metadata.og:description` / `twitter:site` | `description` / `seo.x.handle` | other `og:*`/`twitter:*` drop: Blume writes them |\n| `announcement.message` | `banner` | a string, or `{ content, link: { text, href } }` |\n| `analytics.*` | the matching factory from `blume/analytics` (`posthog`, `googleAnalytics`, `googleTagManager`, `segment`), or `script()` | a `${VAR}` value → `process.env.VAR` |\n| `js` (a path, `{ path, strategy }`, or `{ url, strategy }`) | `script({ src \\| content, strategy })` per tracking script | a third-party loader ports; code that reads or restyles Fern's DOM (`.fern-*`) doesn't (report it). Fern's strategies (`beforeInteractive`, `afterInteractive`, `lazyOnload`) aren't Blume's (`async`, `defer`): pick one or leave it out. A `<link rel=\"help\" href=\"/llms.txt\">` injection drops: Blume publishes `llms.txt` |\n| `css` | a root `theme.css` | `.fern-*` selectors match nothing in Blume; report what you drop |\n| `default-language`, `settings.http-snippets` | `codeSamples` on `openapi()` | that language first; a list → that list; `false` → `false` |\n| `settings.disable-search` | `search: false` |  |\n| `layout.hide-feedback` | `feedback: false` |  |\n| `ai-search` / `ai-chat` (Ask Fern) | **drop** (report) | Blume's assistant is opt-in and self-hosted (`ai.assistant`, a provider key, server output); offer it, don't enable it silently |\n| `agents.*` | **drop**, or `agents.llmsTxt.details` for directive text | Blume generates `llms.txt` and `robots.txt` |\n\n## Navigation\n\n### Fern's URL rules\n\nThe codemod applies these; you need them for its sitemap diff and anything it doesn't handle. Each level contributes a slug, joined outermost first: **product → version → tab → sections → page**.\n\n- **Each slug is the item's `slug:` if set, else lodash `kebabCase` of its display name**: tab `display-name` (not the tab's key), `section:` title, `page:` name. `kebabCase` lowercases, drops punctuation, and splits camelCase and letter/digit boundaries: `API Reference` → `api-reference`, `AgentMail` → `agent-mail`, `SOC 2` → `soc-2`, `v3 (Latest)` → `v-3-latest`.\n- **`skip-slug: true`** on a tab, section, or api entry drops that level from the URL but keeps it in the sidebar. A changelog tab ignores it.\n- **A frontmatter `slug` is the page's whole URL below any product/version prefix**: it replaces the tab and section slugs too. A `docs.yml` `slug:` replaces only that item's own segment.\n- **An `api` entry's base** is its `summary` page's frontmatter `slug` when set; otherwise its `slug` (or `kebabCase(title)`) under the tab.\n- **A changelog** serves its index at its slug and **each entry at `<changelog>/<YYYY>/<M>/<D>`** (no zero padding), titled with the date (`September 5, 2026`). Two entry files dated the same day share one URL: Fern merged them into one page.\n- **`/`** answers with a 307 to the first page when no landing page is set. **API group paths** (`/api-reference/inboxes/threads`) answer with a 307 to the group's first endpoint and aren't in the sitemap, but content links to them.\n- **The first version is the default and has no version segment**; every other version is prefixed with its slug. Products always carry their slug.\n\n### What the codemod builds\n\n- **One folder per tab** at the tab's URL prefix. The tab whose pages share no single prefix (a `skip-slug` tab, or one whose pages all carry root-level frontmatter slugs) becomes the content root, with a tab at `path: \"/\"`. Only one tab can be the root; the codemod reports a second one, which must move under a prefix (route change, redirects).\n- **A section** whose pages share a URL segment → a real folder; one that adds no segment → a `(group)` folder. A section's `path` (its overview page) → that folder's `index.mdx`.\n- **A page whose URL departs from its folder** (a frontmatter `slug`) stays in its sidebar folder with `slug` set, since Blume places a page in the sidebar by file path, not route.\n- **A file `docs.yml` lists twice** is published once, at its first listing; the other URL redirects there (reported).\n- **`meta.ts`** per folder: `pages` in `docs.yml` order, the section title and Lucide icon, and a collapsible section (`collapsed`, `collapsible`, `collapsed-by-default`) → `display: \"group\"`. Blume lists loose pages above subgroups, so a section that interleaves pages and subsections renders its pages first: report any such reorder.\n- **Labels.** Fern labels a page in the sidebar with its `page:` name (or `sidebar-title`) and titles it with frontmatter `title`. So: no `title` → `title: <page name>` (or an H1 that opens the body, which it removes); a different `title` → `sidebar.label: <page name>`. The api **summary** page keeps its `title` with no label (its page name usually repeats the tab label).\n- **`hidden: true`** → `hidden: true`, `seo: { noindex: true }`, and `search: { exclude: true }`: Fern keeps hidden pages reachable but out of search and indexing.\n- **The root URL.** Nothing serves `/` under a root tab, so it redirects `/` to the first page (307), as Fern did; set `logo.href` to that page. The tab at `/` links to the sidebar's first page on its own. A root `index.mdx` would move the landing page to `/`; do that only if the user wants `/` as the landing URL.\n\nBy hand: `availability` (`beta`, `deprecated`, …) → `sidebar.badge` (`Beta`) or `deprecated: true`; `- link:` items → `navigation.featured` (`{ label, href, icon? }`) or an explicit `navigation.sidebar` link (report which); a hidden **tab** (its printed line ends `(hidden on Fern)`) → leave it out of `navigation.tabs`; the codemod already marked its pages hidden.\n\n### Tabs\n\nThe codemod prints one `navigation.tabs` entry per tab (`{ label: display-name, path, icon }`). Keep them as header tabs.\n\n- An **`href` tab** (Blog, GitHub, a dashboard) has no content → `navigation.actions` or `navigation.featured`; a GitHub tab → `github`.\n- **Tab `variants`** (several sidebars under one tab): the codemod reports them. One subfolder per variant and a tab with `items` (`{ label, path }` per variant); variant pages move, so add redirects and report the approximation.\n\n### Changelog\n\nThe codemod moves each entry to `changelog/<YYYY-MM-DD>[-suffix].mdx` (`MM-DD-YYYY` and `MM-DD-YY` names are renamed, so entries sort by date and a two-digit year's month isn't read as an ordering prefix. Fern parsed those in the publishing machine's time zone, so one can sit a day off: the sitemap diff shows it) with `type: changelog`, `date`, and a `title`: the frontmatter one, the date as Fern showed it, or the file suffix sentence-cased (`agentid-sign-in-keys` → \"Agentid sign in keys\"). **Review every suffix title** (`AgentID sign-in keys`); the TODO list names them. `tags` → `changelog.category` (the first) and `search.tags` (all).\n\n- **Same-day entries.** The day's Fern URL redirects to the unsuffixed entry, or else the first by file name. Fern showed both on one page: consider linking the second from the first, and report it.\n- `overview.mdx` (or `index.mdx`, `summary.mdx`) → its text goes into `changelog: { title, description }` in `blume.config.ts`, since an `index.mdx` would replace Blume's generated `/changelog` index (a list, where Fern's index rendered whole entries). Its `layout` and `authors` drop.\n- A non-date file (`TEMPLATE.mdx`) was never published: move it out or `_`-prefix it, and update it to Blume's format if contributors copy it.\n- The feeds `/<changelog>.rss` (`.atom`, `.json`) redirect to `/changelog/rss.xml`, which needs a site URL.\n- For a public GitHub repo, offer `githubReleases()` as SKILL.md \"Changelogs\" says; keep the files when the user says the entries are curated rather than release notes.\n\n### Versions and products (by hand, untested)\n\n- **`versions`** → Blume's native versioning (`docs/content/versioning.mdx` in the installed package). The first Fern version is the live tree at the content root; every other version → a folder named by its slug, listed in `versions.archived`. Place each version file's `navigation` with the same rules; a `ref:` version lives in git, so check it out. Per-version API references → one `openapi()` each, with its own `route`.\n- **`products`** → one folder per product plus `navigation.selectors: [{ kind: \"product\", label, items }]`.\n- `<If versions products>` → keep each folder's matching branch.\n\n## Content and components\n\nThe codemod writes every page as `.mdx` and converts:\n\n- **Callouts.** `<Note>`, `<Tip>`, `<Info>`, `<Warning>`, `<Success>`/`<Check>`, `<Error>`, `<Launch>`, and `<Callout intent>` (no intent → `info`, as Fern rendered it). A top-level one becomes a `:::type[Title]` directive (dedented). One whose icon maps to Lucide (a directive can't set one), or one nested in a component, becomes `<Callout type title icon>`; an icon with no Lucide equivalent drops (reported).\n- **Code groups.** `<CodeBlocks>` → `<CodeGroup>`; every untitled fence inside a group gets Fern's language title (`title=\"Python\"`, `title=\"cURL\"`). Blume labels an untitled grouped fence by its language anyway, so the title only keeps Fern's spelling. `<CodeBlock title>` wrappers are unwrapped onto the fence: Blume's `<CodeBlock>` renders a wrapped fence, but not the wrapper's `title`.\n- **Fence metas.** Multi-word bare titles are quoted (Fern and Blume both read every word of ` ```js Snippet with title` as the title), `filename=` → `title=` and `wordWrap` → `wrap` (Blume warns `BLUME_CODE_FENCE_OPTION` on the Fern spellings), `env` → `dotenv` (Shiki has no `env`: `BLUME_UNKNOWN_CODE_LANGUAGE`).\n- **Cards and accordions.** `<Cards>` → `<CardGroup>`, card icons → Lucide, Fern-only card props drop. `<AccordionGroup>`/`<Accordion>` → `<Accordion>`/`<AccordionItem>`; a lone `<Accordion>` is wrapped in one.\n- **`<Icon>`**: icon → Lucide, `size` × 4 (Fern counts in 4px units). **`<Frame>`**: `background` drops. **`<Tab language>`**: `language` drops (Blume syncs same-titled tabs).\n- **`<ParamField path>`** → `<ParamField name>`: Fern's `path` is the field's name, and Blume's `path=` would mark it a path parameter. `toc` drops. Nested fields in an `<Indent>` (reported) → an `<Expandable title=\"properties\">` in the parent field's body.\n- **Snippets.** `<Markdown src=\"/snippets/x.mdx\" planName=\"x\" />` → `<include planName=\"x\">/_snippets/x.mdx</include>` on a line of its own, with the snippet moved to `<content-root>/_snippets/`. Its `{{planName}}` placeholders carry over as written; Blume reads an include prop only when its name starts with a lowercase letter, so rename any that don't.\n- **Embeds and comments.** A YouTube iframe → `<YouTube id>` (a playlist embed → `<YouTube url>`); `allowfullscreen`/`frameborder` → `allowFullScreen`/`frameBorder` (MDX drops the lowercase forms silently); `<!-- -->` → `{/* */}`.\n- **Assets.** `/assets/x.png` and `../x.png` → moved into `public/` at their path from `fern/` and linked root-absolute. A YAML or JSON file is copied instead (SDK generation may read it; the copy goes stale when it changes). An asset no page or config references stays put: move or delete it.\n- **Links.** Absolute links to the docs' own domain → root-relative; links to moved pages, endpoints, and Fern's `api:METHOD/path` syntax → the new routes (the `endpoints` step).\n\n**Reported for you to convert** (a leftover Fern tag gets a `BLUME_UNKNOWN_COMPONENT` warning, or crashes the page):\n\n| Fern | Blume |\n| --- | --- |\n| `<EndpointRequestSnippet endpoint=\"POST /x\">`, `<EndpointResponseSnippet>` | a hand-written fenced sample plus a link to the operation page: Blume can't embed an operation's sample in prose |\n| `<EndpointSchemaSnippet>`, `<Schema>`, `<SchemaSnippet>`, `<WebhookPayloadSnippet>`, `<RunnableEndpoint>` | a link to the operation page; a small schema can become a `TypeTable` |\n| `<Badge intent outlined>`, `<Availability type>` | `<Badge variant stroke>` (`success`, `warning`, `error` → `danger`, `tip` → `accent`); an availability that labels the page → `sidebar.badge` |\n| `<Code src title>` | `<include meta='title=\"…\"' lang=\"…\">./path</include>` with the file inside the content root |\n| `<Files>`/`<Folder>`/`<File>` | `<Tree>`/`<Tree.Folder>`/`<Tree.File>` |\n| `<Versions>`/`<Version title>` | `<Tabs>`/`<Tab title>` |\n| `<If>` (roles), `<Feature flag>` | the public or default content; ask before dropping role-gated text |\n| `<Button href>`, `<Download src>`, `<Copy>` | a link (or `<Card href>`), a link to the file in `public/`, inline code |\n| `<Aside>`, `<Indent>`, `<StickyTable>`/`<SearchableTable>`/`<PaginatedTable>` | the content inside |\n| `<Prompt title>`, `<ScrollWalkthrough>` | `<Prompt description>`; `<Steps>` with a code block per step |\n| `<ChangelogTags>`, `<Template>` | delete (report) |\n\nNot reported, but check: headings inside `<Steps>` (Fern makes each a step) → one `<Step title>` each; `<Anchor id>` → `<a id=\"x\"></a>`; `<llms-only>`/`<llms-ignore>` → `<Visibility for=\"agents\">`/`<Visibility for=\"web\">`; `maxLines` → `expandable`; inline `$…$` math → `$$…$$`; custom React components → an island or a `components.ts` override. **Unclosed fences** (Fern rendered an empty block): the codemod closes them at the end of the file and reports them; delete the stray fence.\n\n### Frontmatter\n\nFern ignores unknown keys; Blume's schema is strict. The codemod maps `title`, `sidebar-title` → `sidebar.label`, `subtitle` → `description` (Blume renders `description` under the title), `description` → `seo.description` when `subtitle` took `description`, `headline` → `seo.title`, `slug`, `noindex` → `seo.noindex`, and the `docs.yml` `icon`. It reports every other key; map these by hand:\n\n| Fern | Blume |\n| --- | --- |\n| `canonical-url` | `seo.canonical` (absolute) |\n| `image`, `og:image` | `seo.image` |\n| `keywords` | `search.keywords` (a list) |\n| `last-updated` | drop it everywhere and set `lastModified: \"git\"` site-wide; retire any workflow that stamps it, since the key now fails the build |\n| `layout` | `mode`: `overview`/`reference` → `wide`, `page` → `center`, `custom` → `custom`, `guide` → drop |\n| `hide-nav-links: true` | `pagination: false` |\n| `availability` | `sidebar.badge`, or `deprecated: true` |\n| `position` (orders pages in a `- folder:` entry) | `sidebar.order` |\n| `hide-toc`, `force-toc`, `max-toc-depth`, `hide-feedback`, `hide-page-actions`, `no-image-zoom`, `breadcrumb`, `search-metadata`, `edit-this-page-url`, `logo`, `nofollow`, `jsonld:breadcrumb`, `og:*`, `twitter:*`, `excerpt`, `tags` | drop (report) |\n| keys Fern never read (`sidebar_position`, `lastUpdated`) | drop |\n\n### Icons\n\nFern uses Font Awesome; Blume is Lucide-only. The codemod normalizes Fern's spellings (`fa-solid fa-bolt`, `solid bolt`, bare `bolt`), maps them to Lucide names Blume bundles, and reports the rest. Most brand icons (`discord`, `google`, `aws`, `python`, `node-js`, `x-twitter`) have no Lucide equivalent: they drop. An image path on a component (`icon=\"./icons/x.svg\"`) → `public/` and a root-absolute path; one in `docs.yml` is reported for you to move. For icons you write yourself (config, hand conversions), check each name from the folder holding `blume.config.ts`; an unknown icon renders nothing:\n\n```bash\nnode -e \"const p=require.resolve('@iconify-json/lucide/icons.json',{paths:[require('path').dirname(require.resolve('blume/package.json'))]});const j=require(p);for(const n of process.argv.slice(1))console.log(n,n in j.icons||n in (j.aliases||{}))\" zap mail-check discord\n```\n\n## API reference\n\nEach `- api:` entry → one `openapi()` (or `asyncapi()`/`graphql()`) source from `blume/reference`, with **`route` set to the api entry's old base URL** so the overview keeps its address, and a tab at that route. Find the spec each entry renders:\n\n- `specs:` on the api entry → those files (`type: openapi | asyncapi | graphql`).\n- Otherwise `api-name: <name>` → `fern/apis/<name>/`, or `fern/` with one API. There, `generators.yml` `api.specs` lists OpenAPI/AsyncAPI files (older projects use top-level `openapi:`/`async-api:`); **a `definition/` folder is a Fern Definition**, which the codemod's dry run names.\n\n### OpenAPI and AsyncAPI inputs\n\n- **Point `spec` at the file where it lives** (SDK generation reads it too).\n- **Fern `overrides`** are Fern's deep-merge format, not OpenAPI Overlays. Most hold only `x-fern-*` SDK extensions, which Blume ignores; content an override changes for readers → an Overlay in `overlays`. Fern's own `overlays:` files carry over.\n- **`x-fern-ignore: true` operations would appear** in Blume: remove them with an overlay action (`target: $.paths.*[?@['x-fern-ignore'] == true]`, `remove: true`), likewise operations an api entry's `audiences` filtered out.\n- gRPC and OpenRPC references have no Blume renderer: report them.\n\n### Fern Definition inputs → export to OpenAPI\n\nBlume can't read a Fern Definition. Export it with the Fern CLI, which runs offline with no login:\n\n```bash\nnpx fern-api@<version from fern/fern.config.json> export --api <api-name> openapi/<api-name>.yml\n```\n\n- The output is **OpenAPI 3.0.1** (Blume upgrades it). Operation ids are `<package path>_<endpoint>` (`inboxes_threads_list`); each Fern package becomes one PascalCase tag (`InboxesThreads`); `info.title` is the API's name with an empty `version`.\n- **It drops webhooks, WebSocket channels, and `idempotency-headers`.** Restore the first and last with the overlay below; a WebSocket channel has no Blume page, so redirect its old URL to the guide that covers it and report it.\n- **A committed export.** If the repo already commits an OpenAPI generated from the definition (a `fernapi/fern-openapi` generator group writing locally, or a CI step running `fern export`), point `spec` at it, then check it isn't stale: export to a scratch path and diff. If they differ, report it and refresh it with the repo's own command (`fern generate --group <group> --local`), not a second copy. Look for workflows that trigger on that file (a release pipeline) before committing a refresh. Only when nothing produces a committed file, commit your export and add a `\"docs:spec\"` script that reruns it.\n\nDocs-only changes go in an overlay so the spec stays regenerable. The agentmail overlay, abridged:\n\n```yaml openapi/docs.overlay.yaml\noverlay: 1.1.0\ninfo: { title: Docs fixes, version: 1.0.0 }\nactions:\n  - target: $.info\n    update: { title: AgentMail API }\n  # Tags are flat in Blume: name the concatenated package tags.\n  - target: $.paths.*.*.tags[?@ == 'InboxesThreads']\n    update: Inbox Threads\n  # Fern's sidebar order.\n  - target: $\n    update:\n      tags: [{ name: Inboxes }, { name: Inbox Threads }]\n  # Webhooks the export dropped: their payload schemas survive in components.\n  - target: $\n    update:\n      x-webhooks:\n        messageReceived:\n          post:\n            summary: Message Received\n            tags: [Webhook Events]\n            requestBody:\n              content:\n                application/json:\n                  schema: { $ref: \"#/components/schemas/MessageReceivedEvent\" }\n            responses:\n              \"200\":\n                description: Return a 2xx status to acknowledge the event.\n  # One action per `idempotent: true` endpoint in the definition.\n  - target: $.paths['/v0/inboxes/{inbox_id}/messages/send'].post.parameters\n    update:\n      - name: Idempotency-Key\n        in: header\n        schema: { type: string }\n```\n\n**Restore webhooks this way** rather than redirecting their pages: one `x-webhooks` entry per webhook in the definition, keyed by its name, with a `$ref` to its payload schema. Blume upgrades `x-webhooks` to `webhooks` and gives each a page at `<route>/<tag-slug>/<kebab-name>`, which the `endpoints` step matches to the old URL. Nested packages (`Inboxes › Threads`) become sibling tags: report the lost nesting.\n\n### Layout, summary, snippets, Try it\n\n- **`summary`** (a page shown at the API's root) → the codemod places it at `<route>/introduction.mdx`, a normal page in the reference sidebar. The old base URL serves Blume's overview, which renders `info.description`.\n- **`layout`**, `alphabetized`, `flattened`, `paginated` → Blume orders tag groups by the top-level `tags` list (set it in the overlay) and operations within a tag in the spec's order (paths as listed, each path's methods as written). An endpoint order that `layout` or `alphabetized` set and the spec doesn't share → a `pages` list of operation slugs in `<route>/<tag-slug>/meta.ts`, or report it. An endpoint's `title` → its `summary` via overlay; `hidden: true` endpoints → remove them or let them show (ask).\n- **`snippets`** (SDK package names) → Fern rendered SDK calls; Blume generates HTTP-client samples. Add `x-codeSamples` through the overlay to keep SDK calls, or report the loss.\n- **Try it.** Fern's API Explorer proxies requests; Blume's playground sends them from the browser, so the API must answer CORS for the docs origin. Probe it:\n\n  ```bash\n  curl -si -X OPTIONS https://<api-host>/<path> -H 'Origin: https://<docs-domain>' -H 'Access-Control-Request-Method: GET'\n  ```\n\n  No `Access-Control-Allow-Origin` in the answer → `playground: false`, and report it; the alternative is `playground: { proxy: true }`, which needs server output and a host adapter (SKILL.md). `environments: []` → `playground: false`; `oauth: true` → Blume takes a pasted token and doesn't run the flow (report).\n\n### API URLs\n\nThe `endpoints` step computes these; check its output against them.\n\n- **Fern Definition**: the operation id split on `_`, each part kebab-cased, under the base: `inboxes_threads_list` → `/api-reference/inboxes/threads/list`.\n- **OpenAPI**: with `x-fern-sdk-method-name`, `<base>/<x-fern-sdk-group-name>/<x-fern-sdk-method-name>`; otherwise `<base>/<first tag>/<the operation id minus the tag's leading words>`, or the summary when there's no operation id. Each part kebab-cased (`v1` → `v-1`). Read from Fern's parser, not from a migrated site: the `endpoints` step's coverage check catches any URL it gets wrong.\n- **Group paths** → the overview's anchor for that tag: `/api-reference/inboxes/threads` → `/api-reference#inbox-threads` (307, as Fern's was). Content linking to a group gets the same anchor; `blume validate` checks it.\n\nBlume routes rarely match Fern's (the `endpoints` step skips any that do), and a pattern redirect over the old prefix also covers the new pages (`BLUME_REDIRECT_MATCHES_PAGE`), hence one exact redirect per endpoint. Don't shorten operation ids with an overlay: OpenAPI needs them unique across the whole spec, and shortened ones repeat (`list` in every tag), which warns `BLUME_OPENAPI_DUPLICATE_OPERATION_ID`.\n\n## URLs, redirects, and deploy\n\n- **`docs.yml` redirects** → `{ from, to, status }`: Fern's `permanent` defaults to true (308), `permanent: false` → 307; Blume's own default is 301, so the codemod writes the status. `:name` and `:name*` carry over; `:name+` and regex groups are reported (expand them into exact entries). A destination that moved is followed to its final route, and a `:slug*` pattern into the reference expands into one entry per operation. Trailing-slash duplicates merge.\n- **A destination that's neither a page nor another redirect** was already broken on Fern (the TODO list names it): point it at the intended page and report it.\n- **`blume validate --strict` finds links that already 404 on Fern.** Fix the link, don't add a redirect for it, and report the list.\n- **A subpath instance** (`acme.com/docs`): sources, destinations, and links carry the subpath; strip it, since Blume adds `base` itself. Serving both a domain and a subpath means two builds.\n\n## Teardown (keep SDK generation)\n\n**Keep**, even with no docs left in it: `fern/fern.config.json`, every `generators.yml`, the Fern Definition, every spec, override, and overlay a `generators.yml` references, and CI steps that run `fern check`, `fern generate --group <sdk>`, or `fern export` (with the `FERN_TOKEN` secret if they use it).\n\n**Remove**, once migrated: `fern/docs.yml`, redirect YAML files, the docs folders that moved (pages, `changelog/`, `snippets/`, assets, docs `components/`, `css`/`js`), CI steps and wrapper scripts that publish or preview the docs (`fern generate --docs`), and the `fern-api` dependency only if nothing else uses the CLI.\n\nThen **grep the repo for the old paths and syntax**: `fern/pages`, `fern/changelog`, `fern/snippets`, `docs.yml`, `<CodeBlocks>`, `<Note>`, `last-updated`. Contributor docs (`AGENTS.md`, `CONTRIBUTING.md`, templates) and generators that write into `fern/snippets/` need the new paths, frontmatter, and components. Run `fern check` afterwards.\n\nRepoint `package.json` scripts to `blume dev`/`build`/`preview`, add `blume`, and regenerate the lockfile (SKILL.md step 6). Tell the user what can't be committed: moving the custom domain's DNS (or the proxy behind a subpath instance) off Fern, then unpublishing the site in the Fern dashboard.\n\n## Dropped: report these\n\n- **Hosted features**: the API Explorer's proxy, login-injected credentials, and OAuth flow; Ask Fern search and chat; AI-generated examples; **Fern's docs MCP server (`/_mcp/server`)**, which has no static equivalent (Blume's MCP server is at `/mcp` and needs server output; `.md` page mirrors and `llms.txt` carry over), so update content that advertises it; the dashboard, analytics, preview links, RBAC and authenticated docs (host-level protection instead, per the deployment docs' **Private docs** section), feature flags.\n- **API reference**: SDK snippets (unless rebuilt as `x-codeSamples`), WebSocket channel pages, a `layout` or `alphabetized` endpoint order you didn't rebuild with `meta.ts`, nested sections, hidden endpoints, gRPC/OpenRPC references.\n- **Navigation and theme**: dropdown and link icons, approximated variants, sidebar reorders, moved `link` items, extra `colors`, `layout`, custom `header`/`footer`, `css` aimed at `.fern-*`, and `js` that isn't a tracking script.\n- **Content**: API embeds replaced by samples or links, code tooltips, `<ChangelogTags>`, role gating, and each dropped frontmatter key.\n- **Icons** with no Lucide equivalent, by name and where they were used.\n- **Unpublished drafts** and what was done with them.\n- **Second instances** and what replaces them.\n",".agents/skills/blume-migrate/references/fumadocs.md":"# Fumadocs → Blume\n\nFumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present.\n\n## Detect\n\n- Content under **`content/docs/`** (the Fumadocs convention).\n- Per-folder **`meta.json`** files.\n- `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps, and collections declared either in a `source.config.ts` (the Config API) or, in newer scaffolds, with `defineDocs` from `fumadocs-mdx/macro` inside `lib/source.ts` (no `source.config.ts` at all).\n- A `loader({ baseUrl })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`). Newer scaffolds pass a constant (`baseUrl: docsRoute`) defined in `lib/shared.ts` — resolve it there.\n\n## Config\n\nFumadocs declares little Blume needs — read the loader and the shared layout options:\n\n- **Title** — the header title: `nav.title` in `baseOptions` (`lib/layout.shared.tsx`, or `app/layout.config.tsx` in older scaffolds; often the `appName` constant from `lib/shared.ts`; extract the text if it's JSX). Without one, read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case); for a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead.\n- **Header settings** (the same `baseOptions`): `githubUrl` → `github: { owner, repo }` (the repository and edit links; when it's built from a `gitConfig` constant in `lib/shared.ts`, `{ user, repo, branch }` → `{ owner: user, repo, branch }`); `links` entries → `navigation.actions` (`{ label: text, href: url }`), except a `type: \"icon\"` link to a platform `footer.socials` knows (Discord, X, …), which goes there instead. Menus, custom items, and JSX in `nav` → report.\n- **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: \"/docs\"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `\"/\"`, serve from the site root (nothing to set).\n\nEverything else is Blume defaults.\n\n**Also read the MDX options** — remark/rehype plugins live in `source.config.ts` `mdxOptions`, or, with the macro API, in the collection options passed to `defineDocs` (a macro project may still keep a `source.config.ts` for global options):\n\n- `remark-math`/`rehype-katex` → nothing to configure: `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field), as a block or inline within a sentence. A single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence.\n- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.\n- Custom Shiki transformers/themes or other plugins → report.\n\n## Navigation: `meta.json` → `meta.ts`\n\n**Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`):\n\n| Fumadocs `meta.json` | Blume `meta.ts` |\n| --- | --- |\n| `title` | `title` |\n| `icon` | `icon` — **convert the casing** (see Icons below) |\n| `defaultOpen: false` | `collapsed: true` |\n| `defaultOpen: true` | `collapsed: false` |\n| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |\n| `pages: [...]` slugs | `pages: [...]` (ordering) |\n| `description` | **drop** (folders have no description) |\n| `collapsible: false` | that folder's `display: \"flat\"` (a plain heading, even under a global `\"group\"` mode) |\n\n`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`, `directory` (`\"card\"`/`\"accordion\"` lists the folder's pages below its `index` page — the replacement for a hand-written card list of the folder's children). Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: \"group\"` (drill-in is `\"page\"`); when the whole sidebar should collapse, set `navigation.sidebar.display: \"group\"` once in `blume.config.ts` instead of repeating it per folder.\n\n**Numeric filename prefixes move URLs.** Fumadocs builds slugs from the file path as written, so `01-intro.mdx` serves `/docs/01-intro`; Blume reads a `01-` prefix on a file or folder as sort order and drops it from the route (`/docs/intro`). Add a `redirects` entry for every prefixed file or folder (or pin the old path with frontmatter `slug`, a full path from the content root), and write `pages` entries without the prefix (`\"intro\"`, not `\"01-intro\"`).\n\n**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.\n\nHandle the `pages` array items:\n\n- **`\"...\"`** (rest marker) / `\"\"` → drop; Blume appends unlisted pages automatically. **`\"z...a\"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.\n- **A `pages` list with no `\"...\"`** → Fumadocs shows **only** the listed items; every page and folder it leaves out is hidden from the sidebar (still served by URL). Blume appends unlisted children, so each one **resurfaces** — set `sidebar.hidden: true` in each left-out page's frontmatter (for a left-out folder, in every page under it — a folder with no visible page drops out of the sidebar).\n- **`\"!page\"`** (exclusion from `\"...\"`) → the same trap: Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead.\n- **`\"---Section---\"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead.\n- **`\"...folder\"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.\n- **`\"[Text](url)\"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`).\n- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.\n\n## Frontmatter\n\nFumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). **`full: true`** (full width, TOC folded into a popover) → `mode: wide` (full column width, no TOC — the closest match; report); **`_openapi`** marks a generated API stub — delete the whole page (see OpenAPI). Any other non-schema key is a build error, so drop and report.\n\n## Icons: lucide-react names → kebab-case\n\nFumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`\"BookOpen\"`, `\"HomeIcon\"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons).\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare `<Callout>`** → `:::info`. `title` → `:::type[Title]`. An `icon` (a JSX element there) needs a `<Callout type=\"…\" title=\"…\" icon=\"cpu\">` with its kebab-case Lucide name, since a directive can't set one; drop it if it only restated the type's icon.\n- **Cards:** `<Cards>` → `<CardGroup>`. `<Card>` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={<Cpu />}` imported from `lucide-react`) → Blume takes a string name (`icon=\"cpu\"`); Fumadocs `description=\"…\"` → Blume has no `description` prop — move the text into the card body.\n- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).\n- **File trees:** `<Files>` → **`<Tree>`** (the JSX container for `Tree.*` children — _not_ `<FileTree>`, which wraps a Markdown list); `<Folder>` → `<Tree.Folder>` (`defaultOpen` carries over); `<File>` → `<Tree.File>`. Or convert the whole block to a list-driven `<FileTree>`.\n- **Tabs:** Fumadocs declares labels on the parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value=\"npm\">`. Blume's `<Tab>` carries its own `title`. Strip `items={[…]}` from `<Tabs>` and give each child `<Tab>` a `title` (from its `value`, or the positional `items` entry).\n- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.\n- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link: { href, text }, dismissible, id }`).\n- **`<include>./partial.mdx</include>`** — Blume uses the same include syntax. **Pass through unchanged**: paths resolve relative to the including file, a leading `/` resolves from the content root, and nested includes work. Keep partials in `_`-prefixed files or folders so they stay out of routing and the sidebar.\n- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.\n- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory the MDX components file before deleting it** — `components/mdx.tsx` (`getMDXComponents`) in newer scaffolds, `mdx-components.tsx` in older ones; components registered there are used import-free in MDX bodies; port or inline each usage first.\n\n## Headings\n\nTrailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from the TOC), `[toc]` (TOC-only entry) — use the same syntax in Blume. **Pass through unchanged.** One exception: Fumadocs' looser grammar accepts an id containing whitespace (`[#two words]`); Blume does not parse that as a marker, so rewrite such an id to a hyphenated one and update every link that targets it.\n\n## Code fences\n\n- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.\n- Adjacent fences sharing `tab=\"…\"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.\n\n## OpenAPI\n\n`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list (vendor the spec locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.\n\n## GraphQL\n\n`@fumadocs/graphql` works differently from the OpenAPI flow: **no stub files** — pages are virtual, served through the Loader API. The artifacts to harvest and tear down: `lib/graphql.ts` (`createGraphQL()` — read its `input` for the schema and any per-source routes/labels), the `graphql.staticSource()`/`graphql.loaderPlugin()` wiring in the source config, the `GraphQLPage` component (`createGraphQLPage()`, usually `components/api-page.tsx` — read its playground `endpoint`), and the `@fumadocs/graphql/css/preset.css` import. Map to a `graphql({ spec, endpoint })` entry in the top-level `reference` list (see SKILL.md \"GraphQL\"): SDL files/text and introspection results carry over as `spec` (vendor a URL input locally); a programmatic `GraphQLSchema` instance must be printed to SDL and committed (report); the `createGraphQLPage` playground endpoint becomes `endpoint`. When `createGraphQL()` takes several inputs, or sources carry their own routes, labels, or endpoints, map each one to an entry in the adapter's `sources` (`{ spec, route, label, endpoint }`) instead of the single `spec` shorthand — otherwise a schema is dropped or mounted on the wrong route. Blume groups routes the same way (`<route>/queries/<field>`, `<route>/objects/<type>`), but slugs are re-derived — rewrite inbound links and let `blume validate` catch strays. Then remove `@fumadocs/graphql`, the two lib/component files, and the CSS import. Since there are no generated pages, there is nothing to delete from the content tree — but keep any hand-written conceptual pages under the reference route.\n\n## i18n\n\nA `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's default `dir` parser as-is; locale **file suffixes** (`page.cn.mdx` beside an unsuffixed default-locale `page.mdx`) match the `dot` parser as-is — add `parser: \"dot\"`, no file moves. Under `dot` a folder's `meta.ts` serves every locale, so build it from the default locale's `meta.json` and report any translated folder titles in `meta.<lang>.json`. Report whichever transform you apply.\n\n## Package.json & teardown\n\nRepoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*`/`@fumadocs/*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `lib/source.ts`, `lib/shared.ts`, `lib/layout.shared.tsx` (or `app/layout.config.tsx`), `components/mdx.tsx` / `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).\n\n## Dropped — report these\n\nFolder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full` (approximated as `mode: wide`); header settings from `lib/layout.shared.tsx` with no Blume home; `<DynamicCodeBlock>`/`<InlineTOC>`; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.\n",".agents/skills/blume-migrate/references/gitbook.md":"# GitBook → Blume\n\nGitBook is a hosted editor. This reference covers the Markdown it writes when a space syncs to GitHub or GitLab (Git Sync): `SUMMARY.md` (a space's navigation), `.gitbook.yaml` (where its files live, plus redirects), and, on site-wide Git Sync, `gitbook-docs.yaml` (which space maps to which directory). Theme, logo, header and footer links, custom domain, site redirects, and access control live in the GitBook app, not the repo. Two things set this migration apart:\n\n- **URLs follow `SUMMARY.md`, not the file tree.** Moving a page in the editor changes its URL but not its file. Blume derives route _and_ sidebar from the file path, so the core move is putting each file at its old URL.\n- **Blocks are Liquid-style tags (`{% hint %}`) and raw HTML inside `.md`.** Almost every page becomes `.mdx`, and MDX rejects several things GitBook writes.\n\n## Detect\n\n- **`.gitbook.yaml`** or **`.gitbook.yml`** (GitBook reads both) at the root or in a space directory; **`gitbook-docs.yaml`** for site-wide Git Sync.\n- A **`SUMMARY.md`** (GitBook writes it opening `# Table of contents`; a hand-written one may open `# Summary`), with `* [Title](path.md)` items and `## Group` headings, beside a **`.gitbook/`** folder (`assets/`, `includes/`, maybe `vars.yaml`). `.gitbook.yaml` is optional, so this pair can be the only sign.\n- Pages that open with a `# Title` line and use `{% hint %}`, `{% tabs %}`, `{% content-ref %}`; sync commits titled `GITBOOK-<n>: …`.\n- **Not this format:** `book.toml` + `src/SUMMARY.md` is mdBook. `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit — see [Legacy GitBook CLI](#legacy-gitbook-cli-bookjson).\n\n## First, collect what the repo doesn't hold\n\nDo this while the GitBook site is still published. Nothing moves before step 1.\n\n1. **Old URLs.** `<site>/sitemap.xml` indexes one `sitemap-pages.xml` per published space (`/sitemap-pages.xml`, `/de/sitemap-pages.xml`, …); fetch them all. They list every canonical page, hidden and generated OpenAPI pages included, but not `noIndex`/`noRobotsIndex` pages (take those from `SUMMARY.md`). `<site>/llms.txt` lists the same pages with their `.md` URLs. Save the paths, one per line, outside the content root.\n2. **Site redirects** (Settings → Redirects). The app has no export: `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/redirects?limit=1000` with a token (paginated: while the response has `next.page`, fetch again with `&page=<it>`), or ask the user. With neither, report it as a follow-up.\n3. **Site customization.** `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/customization` with a token. Without one, read the published HTML: logo and favicon URLs go through `~gitbook/image?url=<original>` (decode `url` and download it), the header carries a light logo (`class=\"block dark:hidden …\"`) and, when one is set, a dark one (`hidden dark:block`), and the inline CSS declares the primary color as an RGB triplet, `--primary-original`.\n4. **Spaces not in the repo.** Compare the sitemap index with the repo's directories. A space with `content.directory: null`, or a language or version the site serves but the repo lacks (often GitBook-made translations), has no Markdown here. Ask: export it (Git Sync that space into a branch, or `POST https://api.gitbook.com/v1/spaces/{spaceId}/git/export`) and migrate it; regenerate it with `blume translate` once `i18n` is set; or drop it and redirect its URLs (see URLs and redirects). Report the choice.\n\n## Config\n\nMap what step 3 found or the user supplies; leave the rest at Blume's defaults.\n\n| GitBook | Blume |\n| --- | --- |\n| Site title (`gitbook-docs.yaml` `site.title`) | `title` |\n| Logo (light and dark) | `logo: { image: { light, dark } }`, files in `public/` (SKILL.md covers monochrome SVGs and wordmarks) |\n| Icon (favicon) | `public/icon.svg` or `icon.png` (a dark one → `icon-dark.*`); no config field. An emoji icon has no file: make one or report it |\n| Primary color | `theme.accent` |\n| Default mode, fonts, code theme | `theme.mode`, `theme.fonts.{body,display,mono}`, `markdown.code.theme` (nearest Shiki themes) |\n| Theme style, tint and semantic colors, corner, depth, link, and sidebar styles | drop (report); a root `theme.css` if one matters |\n| Header links; a button-styled one | `navigation.actions`; `navigation.cta` |\n| Announcement | `banner` (`{ content, link, dismissible, id }`) |\n| Footer | `footer.links`, flattened into one row; copyright → `footer.copyright`; group titles and logo drop (report) |\n| Social accounts | `footer.socials` |\n| Social preview image | `seo.image` on the landing page (Blume renders a card for every other page) |\n| Git Sync repository and branch | `github: { owner, repo, branch }`, plus `dir` or `host` when needed; GitLab has no equivalent (report) |\n| Page actions: Markdown and AI providers, MCP, PDF | defaults; `agents.mcp.enabled` plus server output (follow-up); `export` |\n| GitBook Assistant | `ai.assistant` plus server output (follow-up), or drop |\n| Integrations | adapters in `analytics` from `blume/analytics`; others `script()` or report |\n| Custom domain | the DNS record moves to the new host; keep the URL as `deployment.site` unless the host is Vercel or Netlify |\n| Custom subdirectory (`example.com/docs`) | `deployment: { base: \"/docs\" }` |\n| Share links, authenticated access | host-level protection (the deployment docs' **Private docs**); report |\n| `.gitbook/vars.yaml` | `variables` (see Variables) |\n\n**Content root.** `root: ./docs/` is Blume's default `content.root`. With `root: ./`, `git mv` the space (pages, `SUMMARY.md`, `.gitbook/`) into `docs/` — routes are relative to the content root, so they don't change — or scope `content.include` (`references/monorepo.md` §1). Left unscoped, the repo's `README.md` and every stray `.md` publish.\n\n## Navigation: `SUMMARY.md` → folders\n\n### How GitBook builds a URL\n\n- The first `SUMMARY.md` item (`structure.readme`, `README.md` by default) is served at the space root; its own slug (`/readme`) redirects there.\n- Every other page: the space's prefix (see Site structure), then its `## Group`'s slug, each ancestor page's slug, and its own. A page's slug comes from its file name at import (a `README.md` → its folder's name), lowercased; a group's, from its heading (emoji dropped) or the `id` of an `<a id=\"…\">` inside it.\n- **The file's own path doesn't count.** A page filed at `configuration/post-install.md` but listed under group \"Deployment\" → parent `deployment/permissions/README.md` publishes at `/deployment/permissions/post-install`.\n\nCompute every page's URL from `SUMMARY.md`, then check them against the sitemap list; the sitemap wins.\n\n### Put every file at its old URL\n\nOne `git mv` per page keeps both the route and the sidebar:\n\n- A page with no children → `<url>.mdx`; with children → `<url>/index.mdx`; the landing page → `index.mdx` at the content root (`.md` for a page that stays plain). Lowercase every segment: Blume keeps a file name's case, so `OCR.md` would publish at `/OCR` where GitBook served `/ocr`.\n- **`README.md` isn't an index in Blume** (`guides/README.md` publishes at `/guides/README`): every README becomes `index`.\n- Each `## Group` becomes a top-level folder named for its slug.\n- Keep a table of old file → new file → route: links, includes, and image paths are rewritten from it.\n- A file not in `SUMMARY.md` was never published. Delete it, or prefix it with `_` to keep it unpublished, and report it.\n- Delete `SUMMARY.md` once the folders and `meta.ts` files carry its structure; it would publish as a page.\n\n### Order, labels, and display → `meta.ts`\n\n- **Order.** Each folder's `meta.ts` lists its children in `SUMMARY.md` order (`pages`); the content root's lists the loose pages, then the group folders.\n- **Labels.** A page's title is its `SUMMARY.md` link text, and a link title (`[Title](page.md \"Label\")`) → `sidebar.label`. A group folder's `meta.ts` `title` is the heading text, and **a parent page's folder gets `title: <its SUMMARY text>` too** — otherwise the row shows the humanized folder name (`deployment-guides` → \"Deployment Guides\" for a page titled \"Getting started\").\n- **Display.** GitBook shows groups as plain headings and each parent page as one expandable row. Set `navigation: { sidebar: { display: \"group\" } }`, `display: \"flat\"` in each group folder's `meta.ts`, and `sidebar: { hidden: true }` on each parent's `index`: the folder row already links to it, so without this the parent lists twice.\n- **Contents pages.** A parent whose body is empty or only content-refs to its own children → delete those and set `directory: \"card\"` in the folder's `meta.ts`. It inherits: set `directory: \"none\"` on a nested folder that isn't a contents page.\n- **Order Blume can't match.** Loose top-level pages always list above groups and folders. A parent page among loose pages (Overview, Editions ▸, Use Cases) drops below them, and pages after a `***` divider in `SUMMARY.md` (top-level again: `/uninstall`, not `/other/uninstall`) rise above the groups. Move such pages into a parenthesized group folder (`(more)/`, which adds no URL segment) ordered in the root `meta.ts`, or accept the order; report either.\n- **External links** (`* [Website](https://…)`) → `navigation.featured` or `navigation.actions`; report the move.\n- **Hidden pages.** GitBook's `hidden: true` hides a page's whole subtree; Blume's hides one page, so set it on the index and every descendant (a folder whose pages are all hidden leaves the sidebar).\n- A list item that is a YAML block with `type: builtin:openapi` is a generated API reference (see OpenAPI).\n\n### Site structure: `gitbook-docs.yaml`\n\n`site.structure` nests `section-group`, `section`, `space`, and `external-link` entries. Every entry has a `key` (meaningless to Blume), a `title`, and maybe `localizedTitle`; most can carry `draft` or `condition`. Sections and spaces have a `path` and maybe `default: true` (a section group has no `path`); section groups, sections, and external links can have an `icon`; a space can be `hidden` and has `content.directory` (`null` when not synced) and maybe `content.language`. Skip `draft` entries, and treat a `condition` like adaptive content (report it).\n\n- **URL prefixes.** The default section's default space is at the site root. Another space → `/<section>/<space>`; a non-default section's default space → `/<section>`. A site without sections serves its default space at the root and the rest at `/<space>`.\n- **Sections** → a top-level folder each plus a `navigation.tabs` entry (`icon` → Lucide). The default section stays at the content root; add `{ label, path: \"/\" }` to show it as a tab.\n- **Section groups** → a dropdown tab scopes the sidebar by one `path`, so give each section its own tab (the grouping is lost; report) or move them under one prefix (URLs change; pattern redirects).\n- **Spaces within a section:** languages → `i18n` (Blume puts the locale first, site-wide: `/payments/fr/x` → `/fr/payments/x`); versions → `versions` (top-level `/<id>/…`, ids start with a letter); products → a folder each and a `navigation.selectors` dropdown. Redirect the old prefixes.\n- A space's directory can sit inside another's (`./home/fr` in `./home`): separate them, then do the `SUMMARY.md` work per space. A `hidden: true` space is still published but left out of the space switcher: keep its pages, leave it out of `navigation.selectors`, and report it. `localizedTitle` → a per-locale tab label.\n- **External links** (`external-link`, at the root or in a section group) → `navigation.actions` entries (`{ label, href }`; `localizedTitle` → a per-locale `label`, the `icon` drops).\n\n## Content and components\n\n### `.md` or `.mdx`\n\nRename to `.mdx` every page with a `{% … %}` block, `$$` math, a mermaid fence, or a component after conversion. Directives, components, math, mermaid, `package-install`, and `ts2js` are MDX-only: in `.md`, a leftover `{% hint %}` or `:::note` is literal text with a green build (each `{% … %}` tag warns `BLUME_TEMPLATE_TAG` at its line, a `:::note` `BLUME_MD_DIRECTIVE`), and in `.mdx` a leftover `{% … %}` fails it (`BLUME_MDX_SYNTAX`, which `blume validate` and `blume check` report too). Plain pages can stay `.md`; tables, footnotes, fence meta, `<include>`, `{{variables}}`, `[#id]`, and raw HTML work in both. An include is spliced as text, so it's parsed in the including page's format.\n\n### MDX hazards\n\nEach of these fails `blume build` in `.mdx` (`blume check` and `validate` name the ones MDX can't parse as `BLUME_MDX_SYNTAX`, at their line):\n\n- **Unclosed `<img …>` and `<br>`** (`BLUME_MDX_UNCLOSED_ELEMENT` names each). Close them — better, convert images to Markdown.\n- **HTML comments** → `{/* */}`.\n- **A `{` outside code.** GitBook's `\\{{x\\}}` isn't enough; write `` `{{x}}` `` or `\\{\\{x\\}\\}`. Braces inside raw HTML (`<code>CN={{DeviceId}}</code>`, `<td>${EMAIL}</td>`) compile, then fail the render: `blume build` names each as `BLUME_MDX_UNDEFINED_NAME` at its line. Turn `<code>`, `<strong>`, `<a>`, `<p>`, and `<br>` inside table cells into Markdown.\n- **Markdown inside JSX** (`<Tab>`, `<Step>`, `<Frame>`) needs blank lines around it.\n\nGitBook's `\\<` and entities (`&#x20;`, `&#x3C;`) are fine (an escaped `\\<Name` can still warn `BLUME_UNKNOWN_COMPONENT`; the page renders correctly). Strip the U+200B/U+200C characters GitBook leaves at the start of some code lines.\n\n### Blocks\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` (the first line) | frontmatter `title`; delete the line |\n| `{% hint style=\"info\" %}` (`success`, `warning`, `danger`) | `:::info` (`:::success`, `:::warning`, `:::danger`); legacy plugin `tip` → `:::tip`, `working` → `:::note`. A directive takes no `icon`: keep one that matters as `<Callout type=\"<type>\" icon=\"<lucide>\">`, else drop it; inside another directive, give the outer one a longer fence (`::::`) |\n| `{% tabs %}` + `{% tab title=\"X\" icon=\"y\" %}` | `<Tabs>` + `<Tab title=\"X\" icon=\"<lucide>\">`; `fullWidth` drops. Blume switches same-titled tabs together page-wide (`sync={false}` opts a group out) |\n| `{% code title=\"a.js\" lineNumbers=\"true\" overflow=\"wrap\" expandable=\"true\" %}` | fence meta: ` ```js title=\"a.js\" lineNumbers wrap expandable `; `fullWidth` drops. **A fence with no language takes `text`** (` ```text lineNumbers `), or the first option becomes the language (`BLUME_UNKNOWN_CODE_LANGUAGE`) |\n| `<pre class=\"language-x\" data-title=\"…\" data-line-numbers data-overflow=\"wrap\"><code>` | a fence built from those attributes (others drop), entities decoded, `<strong>`/`<em>` stripped |\n| `{% stepper %}` + `{% step %}` | `<Steps>` + `<Step>`. **A step that opens with a heading keeps it as the first line inside `<Step>`**: GitBook publishes it as an anchored heading in the outline, and `title` renders a plain paragraph with no anchor. A bold-line opener → `title` |\n| `{% columns %}` + `{% column %}` | `<Columns cols={n}>` + `<Column>`; widths drop |\n| `<details><summary>T</summary>` (`open`) | `<Expandable title=\"T\">` (`defaultOpen`); a run → `<Accordion>` + `<AccordionItem title=\"T\">`. A title renders inline Markdown, so write a summary's `<code>`, `<strong>`, and `<a>` as Markdown in it: raw HTML there shows as text |\n| `{% content-ref url=\"x.md\" %}`; legacy `{% page-ref page=\"x.md\" %}` | `<Card title=\"<x's title>\" href=\"<x's route>\" />` (the link text is a file name, so look the title up); a run → `<CardGroup>`; links to a parent's own children → `directory: \"card\"` |\n| `[text](x.md#h \"mention\")` | a plain link without `\"mention\"`, its text replaced by the target's title |\n| `{% embed url=\"…\" %}`, often with no `{% endembed %}` | YouTube → `<YouTube url=\"…\" />` (`?t=122` → `start={122}`; `url` ignores `t`); anything else → `<Card title=\"…\" href=\"…\" />`. A caption → `<Frame caption=\"…\">` around it (YouTube's `title` only reaches screen readers) |\n| `{% file src=\"…\" %}caption{% endfile %}` | `[caption](../assets/x.pdf)`, relative to the page: Blume publishes a file a relative link names with the page. For a URL that never changes, put the file in `public/files/` and link `/files/x.pdf` |\n| `<figure><img src alt><figcaption>` | `![alt](relative/path.png)` (optimized); a caption → `<Frame caption=\"…\">` around it; `<div data-with-frame=\"true\">` → `<Frame>`. A raw `<img>` with a relative `src` works too, but unoptimized |\n| `<img class=\"gitbook-drawing\">` | a Markdown image |\n| inline `<img data-size=\"line\">` | a Markdown image (renders at its own size), or `<img src=\"/<public path>\" alt=\"\" height=\"20\" />` to stay text-height (report) |\n| `<picture>` with a dark-mode source | the light image (report) |\n| `<table data-view=\"cards\">` | `<CardGroup cols={3}>` (`data-card-size=\"large\"` → `cols={2}`), a `<Card>` per row: first text cell → `title`, other text → body, the `data-card-target` link → `href`, the `data-card-cover` file → `img`, relative to the page like a Markdown image (`img=\"../assets/cover.png\"`), an `<i class=\"fa-…\">` cell → `icon` |\n| other `<table>` (`data-header-hidden`, `data-search`, cells with `<p>`/`<br>`) | a GFM table (a hidden header's first row becomes the header); a cell's `<ul><li>` can stay inline in `.mdx` |\n| `$$…$$`, a ` ```mermaid ` fence | unchanged, in `.mdx` |\n| Emoji: `:rocket:`, escaped `:ballot\\_box\\_with\\_check:`, `<span data-gb-custom-inline data-tag=\"emoji\">☑️</span>` | the character itself: Blume renders no shortcodes |\n| `<i class=\"fa-x\">:x:</i>` | `<Icon icon=\"<lucide>\" />`, or delete it |\n| `<a class=\"button primary\">`, `<button data-action=\"…\">` | a link or a `<Card>` with `cta`; search and ask buttons drop |\n| `<mark style=\"color:…\">` | `<mark>` or bold; report colors that carried meaning |\n| `## Heading <a href=\"#x\" id=\"x\"></a>` | works as written: the heading takes `x` as its id. `## Heading [#x]` is the tidier spelling |\n| `[^1]` annotations | unchanged: footnotes at the page's end, not hover popups |\n| `{% updates %}` + `{% update date=\"…\" %}` | `type: changelog` pages (SKILL.md \"Changelogs\"), or dated `##` sections |\n| `{% prompt description=\"…\" %}` | `<Prompt description=\"…\">` |\n| `{% if visitor… %}` | keep what the public saw, drop the tags (report) |\n\n### Links\n\n- Every moved file breaks the relative links into and out of it. Rewrite internal links from your table as root-absolute routes (`/deployment/permissions#roles`): Markdown links, content-ref URLs, card `href`s, and `href`s in HTML. A `README.md` target → its folder's route; absolute links to the old domain → routes.\n- Cross-space links `https://app.gitbook.com/s/<spaceId>/<path>` (or `…/o/<orgId>/s/…`) → the target space's prefix plus `<path>`; `GET https://api.gitbook.com/v1/spaces/{spaceId}` gives its `urls.published`. Links into `…/~/changes/…` or to `/broken/pages/<id>` were already broken: fix or report them.\n\n### Images and assets\n\n- Move `.gitbook/assets/` to `assets/` at the space root and replace the string `.gitbook/assets/` with `assets/`, then recompute the paths in every moved page.\n- File names have spaces and parentheses. `![](<../assets/image (1).png>)` and `../assets/image%20(1).png` both work, in a page and in an `_includes/` partial alike: a partial's images are rebased onto the including page in any form.\n- GitBook keeps every upload, so many files in `assets/` may be referenced by nothing. Report them and offer to prune.\n\n### Variables and expressions\n\n- `.gitbook/vars.yaml` → `variables`; `<code class=\"expression\">space.vars.NAME</code>` → `{{NAME}}`.\n- `page.vars.NAME` (from `vars:` frontmatter) has no page scope: write the value inline. Read inside an include, it becomes a prop: `<include name=\"Computer\">/_includes/x.md</include>` with `{{name}}` in the partial (prop names are lowercase).\n- An expression with logic (`a ? \"x\" : \"y\"`) → evaluate it and write the result.\n- Once `variables` is set, an undefined `{{name}}` in prose fails the build: placeholders that are content (`{{DeviceId}}`) go in inline code.\n\n### Includes (reusable content)\n\n- `.gitbook/includes/x.md` → `_includes/x.md` at the content root, and `{% include \"../../.gitbook/includes/x.md\" %}` → `<include>/_includes/x.md</include>` on its own line (a leading `/` resolves from the content root).\n- **Demote every heading in an include one level:** GitBook renders an include's `#` at a page's `##`.\n- Delete includes nothing references. One referenced by ID (`{% include \"/reusable-content/…\" %}`) isn't in the repo: copy it from the app.\n\n### OpenAPI\n\nGitBook has five forms: a `SUMMARY.md` YAML item `type: builtin:openapi` naming an organization spec (`dependencies.spec.ref.spec: <slug>`); `{% openapi src=\"…\" path method %}`; `{% openapi-operation spec=\"<slug>\" path method %}` and `{% openapi-schemas %}`; the older `{% swagger src=\"…\" path method %}`; and hand-written `{% swagger method path baseUrl %}` blocks with `swagger-parameter` and `swagger-response` (oldest: `{% api-method %}`).\n\n- **Specs.** Vendor each one into the repo — a `<slug>`'s file comes from the app's OpenAPI panel or `GET https://api.gitbook.com/v1/orgs/{organizationId}/openapi/{specSlug}/versions/latest/content/raw` — and add `openapi({ spec })` from `blume/reference` with a `navigation.tabs` entry (SKILL.md \"OpenAPI\").\n- **`builtin:openapi`** → set `route` to the old reference's path and keep its parent page's prose under it. Operation URLs change, so redirect each old one in the sitemap (SKILL.md \"OpenAPI\" covers generating them).\n- **Inline operations** → a link or `<Card>` to the generated page. `{% openapi-schemas %}` has no counterpart (report).\n- **Hand-written blocks** → a `<ParamField query|path|header|body=\"…\">` per parameter (a `cookie` has no slot), the responses as titled fences in `<ResponseExample>`, and `api: \"GET /path\"` frontmatter when it's the page's only endpoint (`baseUrl` → `api.server`).\n- `x-codeSamples` carries over; GitBook's other `x-*` extensions are ignored (report).\n\n## Frontmatter\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` / `SUMMARY.md` link text | `title`; where they differ, GitBook published the `SUMMARY.md` text |\n| `SUMMARY.md` link title | `sidebar.label` |\n| `description` (often folded `>-`) | `description` (quote a plain value containing `: `) |\n| `icon` (Font Awesome) | `icon`, in Lucide (see Icons) |\n| `hidden: true` | `hidden: true`, on the page and every descendant |\n| `noIndex: true` | `search.exclude: true` + `noindex: true`, on the page and every descendant |\n| `noRobotsIndex: true` | `noindex: true`, on the page and every descendant |\n| `cover`, `coverY`, `coverHeight`, `tags`, `if` | drop (report); a cover worth keeping → an image at the top of the body |\n| `vars` | inline the values, then drop |\n| keys left by older tooling (`category`, `order`) | drop: GitBook ignored them |\n\n`layout`:\n\n| GitBook `layout` | Blume |\n| --- | --- |\n| every part visible, `width: default` | delete the block |\n| `tableOfContents.visible: false` | `mode: center`; with `title.visible: false` too, `mode: custom` |\n| `outline.visible: false`, or `width: wide` | `mode: wide` |\n| `title.visible: false`, sidebar shown | `mode: frame` |\n| `pagination.visible: false` | `pagination: false` |\n| other parts hidden; a preset (`layout: landing`) | drop (report); for a preset, match the published page |\n\nA Blume mode hides more than one part: `center` and `wide` also hide the outline, and `frame` hides it and the page-end links and footer. Report it where GitBook kept those.\n\nBlume's frontmatter is strict: remove every key the tables drop.\n\n## Icons\n\nGitBook uses Font Awesome names in frontmatter `icon`, `<i class=\"fa-…\">`, tab, hint, and button icons, and section icons. Blume is Lucide-only.\n\n- **Frontmatter:** `node <skill>/scripts/mintlify-codemod.mjs <content-dir>`, then `--write`. Its icon pass is the same Font Awesome → Lucide map, and GitBook frontmatter has none of the keys its other rules touch.\n- **Everything else by hand,** with the table in `references/mintlify.md`. Common GitBook names: `circle-info` → `info`, `clock-rotate-left` → `history`, `sliders` → `sliders-horizontal`, `handshake-angle` → `handshake`, `angles-up` → `chevrons-up`, `puzzle-piece` → `puzzle`, `life-ring` → `life-buoy`, `shield-halved` → `shield`, `file-contract` → `file-text`.\n- **Check every name** against the set Blume bundles; an unknown name renders nothing:\n\n  ```bash\n  node -e 'const b=require.resolve(\"blume/package.json\");const s=require(require.resolve(\"@iconify-json/lucide/icons.json\",{paths:[b]}));for(const n of process.argv.slice(1))console.log(n,!!(s.icons[n]||s.aliases?.[n]))' zap history sliders-horizontal\n  ```\n\n## URLs and redirects\n\nWith every file at its old URL, pages need no redirects. The rest are below. GitBook answers all of them with a 307, except a permanent site redirect (308). Blume's default `status` is 301: keep it for a settled move (`.gitbook.yaml` redirects, recovered automatic redirects), and set `status: 307` where the target may change (group paths, dropped translations).\n\n- **`.gitbook.yaml` `redirects`** (`old/path: new/file.md`, relative to the space) → `{ from: \"/<space prefix>/old/path\", to: \"<new/file.md's route>\" }`.\n- **Site redirects** (`source`, `destination`, `permanent`, `captureWildcard`, `draft`) → `{ from: source, to, status: permanent ? 308 : 307 }`. `destination` is an object, not a URL: `kind: \"external\"` carries `url`; `site-page` (`siteSpaceId`, `pageId`), `site-space`, and `site-section` name ids. Resolve those to the old URL — `GET …/sites/{siteId}/site-spaces` gives each site space's `urls.published` and `space.id`, `…/sites/{siteId}/sections` each section's `urls.published`, and `GET /v1/spaces/{spaceId}/content` each page's `path` within its space — then to the Blume route at that URL. A trailing `*` with `captureWildcard` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`; without it, every match goes to the one `to`. Skip drafts (the list leaves them out by default), and write `from` without the site base.\n- **Automatic redirects.** GitBook quietly 307s every former URL of a moved or renamed page, and lists them nowhere. Recover them: run every historical `SUMMARY.md` (`git log --format=%h -- SUMMARY.md`, then `git show <sha>:SUMMARY.md`) through the URL rule above, add the route each file's path implies, and request each candidate from the live site (`curl -s -o /dev/null -w '%{http_code} %{redirect_url}'`). Each 307 to a page you kept becomes a redirect.\n- **Group paths and the landing slug.** GitBook redirects a group's own path to its first page, and `/readme` to `/`. Add both (`status: 307` for the group paths).\n- **Dropped translations.** GitBook translates group slugs (`/de/zahlungen/…` for group \"Payments\"), so `/de/:path*` → `/:path*` sends most translated URLs to 404s. Diff each language's `sitemap-pages.xml` against the default one, add a pattern per translated group (`{ from: \"/de/zahlungen/:path*\", to: \"/payments/:path*\", status: 307 }`), then each language's catch-all last: patterns are tried in order.\n- **Case.** GitBook matched URLs case-insensitively; Blume doesn't. Lowercased files cover the canonical URLs; report mixed-case inbound links.\n- **Markdown URLs.** GitBook serves `<url>.md`, and so does Blume; redirects move the `.md` URL with its page.\n- Rewrite internal links **before** adding redirects: `blume validate` accepts a redirect's `from` as a link target.\n\n### Heading anchors\n\nGitBook makes heading ids its own way: colons, slashes, and a spaced dash become one hyphen each, dots stay, an id that would start with a digit gets `id-`, and ids stop at 100 characters (`AppConfig:OCSP:UseAuthorizedResponder` → `appconfig-ocsp-useauthorizedresponder`, where Blume writes `appconfigocspuseauthorizedresponder`). Every `#anchor` link GitBook wrote to such a heading fails `blume validate`, and links from other sites land at the top of the page. A rule predicted only 890 of 934 ids on a real site, so read them instead. After `blume build`, from the project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com          # report\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com --write  # pin\n```\n\nIt fetches each old page once (one request at a time, cached in `.pin-heading-ids-cache/`; delete it after), pairs its headings with the build's by text, and appends the old id to each differing source heading — `[#id]` in `.mdx`, `{#id}` in `.md`, `\\{#id\\}` in a partial. Rebuild and rerun until it reports 0. A `NOTE … no source line` is a heading the source doesn't spell out (a component renders it, or it was reworded): pin it by hand. `--only-linked` pins only ids that content links to. A page whose URL changed (a space under a new prefix) is looked up on the old site through `--map`, a JSON list of exact `{ from, to }` entries (patterns are skipped): `dist/blume-redirects.json` works as it is, but a build writes it only when no host adapter is set. Any `BLUME_BROKEN_ANCHOR` left after that was already dead on GitBook: retarget the obvious ones, drop the fragment from the rest, and report them.\n\n### Final check\n\nRequest each old URL from `blume dev` or `blume preview` (both apply pattern redirects) with `curl -sL`, or check it against `dist/` and `dist/blume-redirects.json` (written only when no host adapter is set), then run `blume audit --only redirects` after the build.\n\n## Legacy GitBook CLI (`book.json`)\n\nThe old `gitbook-cli` toolchain and its fork HonKit:\n\n- **`book.json`:** `root`, `structure`, `title`, `description`, `variables`, `plugins`. Map `title` and `description`; `root` → `content.root`; plugins → whatever Blume has built in, else report.\n- **URLs are file paths:** `a/b.md` → `/a/b.html`, a `README.md` → its folder's `index.html`. Keep the files where they are, rename READMEs to `index`, and add an exact redirect from each `.html` URL (a pattern can't strip the extension).\n- **`SUMMARY.md`** nests the same way, but `### Part` headings and `---` lines add no URL segment → `(part)/` group folders. Entries pointing at anchors (`page.md#x`) aren't pages.\n- **Still applies:** `{% hint %}` → directives; `{% include \"./x.md\" %}` → `<include>`; `{{ book.name }}` → `variables` and `{{name}}`; other Nunjucks logic → evaluate and inline; `LANGS.md` → `i18n`, with the default language at the content root; `GLOSSARY.md` → no equivalent (report).\n\n## Teardown\n\n- **Turn off Git Sync** for every mapped space before the converted content reaches the synced branch: GitBook imports each push to it and commits its own edits back. Keep the GitBook site published until the domain moves.\n- Delete `SUMMARY.md`, `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, and the emptied `.gitbook/`.\n- Check CI and scripts that read `.md` files or `.gitbook/assets/`: a Markdown link checker breaks on root-absolute routes and skips `.mdx` (replace it with `blume validate --strict`), and an image de-duplication script that greps `*.md` would now delete images in use. Remove GitBook agent instructions (an `AGENTS.md` block, a vendored GitBook skill).\n- A synced repo usually has no `package.json`: scaffold one (SKILL.md step 6).\n\n## Dropped — report these\n\nSite themes and styles beyond accent, fonts, and mode; footer group titles and logo; page covers; page and update tags; adaptive content and visitor authentication; GitBook Assistant unless the user adds `ai.assistant`; hint icons you didn't keep; button styles and search/ask buttons; `<mark>` colors; `fullWidth` and column widths; non-YouTube embeds as players; dark card covers and theme-aware images; inline image sizing; `{% openapi-schemas %}` and GitBook `x-*` spec extensions; automatic redirects you couldn't recover; case-insensitive URLs; page-group icons; sidebar positions Blume can't match (parent pages among loose pages, `***` dividers, external links); section groups flattened into tabs; spaces and translations not in the repo; integrations with no adapter; site redirects nobody could export; anything set only in the app that the user didn't supply.\n",".agents/skills/blume-migrate/references/github-wiki.md":"# GitHub Wiki → Blume\n\nA GitHub wiki is a git repository beside the main one (`https://github.com/<owner>/<repo>.wiki.git`) that GitHub renders at `https://github.com/<owner>/<repo>/wiki`. It has no config file and no build: every file in a markup format GitHub renders is a page, `_Sidebar.md` and `_Footer.md` add the chrome, and pages link each other with `[[Page Name]]`. Five things set this migration apart:\n\n- **Folders don't count.** A page's URL is its file name, wherever the file sits (`codebase/compiler/Codebase-Compiler-Binder.md` is `/wiki/Codebase-Compiler-Binder`), and relative links and images resolve from `/<owner>/<repo>/wiki/`, never from the file's folder.\n- **`[[Link text|Page Name]]` puts the text first** in Markdown pages. GitHub's docs show the reverse, which is right only in `.mediawiki` pages, and Obsidian's `[[Page|text]]` is the reverse too, so don't point the `obsidian()` source at a wiki.\n- **Blume renders `[[…]]` as literal text.** It warns `BLUME_WIKILINK_UNSUPPORTED` only for a wiki link that names one of the site's pages or has a `|`, so `[[Page that doesn't exist]]` ships with a green build. Every wiki link has to be converted.\n- **The old URLs live on github.com,** which you can't redirect. What you can do there is in URLs and the old wiki below. Never tell the user the old links will redirect, and add no `redirects` for them.\n- **Nothing pins the routes,** so the new site gets clean ones: one flat lowercase route per page, as the wiki had one flat namespace, with the sidebar groups as `(group)/` folders.\n\nScope: Markdown pages fully. Pages in the other formats GitHub renders get converted to Markdown first (Other markup formats). Checked against GitHub's wiki docs, the live rendering of public wikis (microsoft/TypeScript, microsoft/vscode, koreader, ruTorrent, qBittorrent, mpv, gollum, fzf, AdGuardHome) in October 2026, and an end-to-end migration of the microsoft/TypeScript wiki (77 pages). GitHub Enterprise Server wikis use the same files but weren't checked.\n\n## Detect\n\n`blume migrate` can't detect a wiki: name it, `npx blume migrate github-wiki`. A wiki clone has:\n\n- a remote ending in `.wiki.git` (`git remote -v`);\n- `Home.md` (or `Home` in another format) at the root, often `_Sidebar.md` and `_Footer.md`;\n- page files named like URLs (`Getting-Started.md`, `Standalone-Server-(tsserver).md`, sometimes `Roadmap‐2021‐2022.md` with U+2010 hyphens), `[[Page Name]]` links, no config, and usually no `package.json`.\n\n**Not this format:** Docsify keeps a lowercase `_sidebar.md` beside an `index.html` that sets `window.$docsify` (`references/docsify.md`). On macOS and Windows the file system ignores case, so `_Sidebar.md` and `_sidebar.md` are the same name to `ls`, `test -e`, and Node's `existsSync`: check the real case with `git ls-files`.\n\n**Get the pages from the default branch.** `git clone https://github.com/<owner>/<repo>.wiki.git`, then `git remote show origin` names the HEAD branch. GitHub publishes only that branch, so other branches never showed. If the wiki is mirrored from a normal repository (a sync workflow, or a footer saying \"fork it and send a pull request\"), the wiki clone is still what readers saw.\n\n## Where the Blume project goes\n\nThe wiki repository can't host the new site: GitHub runs no Actions in it, serves no Pages from it, and accepts no pull requests. Ask the user where the docs will live:\n\n- **A folder in the main repository** (the usual choice: the docs then change in the same pull requests as the code). Follow `references/monorepo.md` for a code repo's root `package.json`, `dist/`, and workspace.\n- **A new repository**, for docs with their own maintainers.\n\nWithout an answer, convert a copy of the wiki clone in place: `blume.config.ts` and `package.json` at its root, the pages moved into `docs/`, so the user can move the folder as one unit. **Before you start, run `git remote remove origin` in the copy.** Pushing the migrated tree to the wiki would publish `blume.config.ts`, every moved page, and the stubs you prepare later as wiki pages.\n\nWiki history doesn't come along with a plain copy. To keep it for `git log --follow` and blame, bring the wiki in with `git subtree add --prefix=<folder> https://github.com/<owner>/<repo>.wiki.git <branch>` and say so in the report. Every page gets moved and edited either way, so `lastModified: \"git\"` would date every page to the migration commit until it's edited again (Blume follows a rename only when the file didn't change).\n\n## Run the codemod\n\nFrom the copy of the wiki clone (when the docs go into another folder, copy the wiki's files there first and run it from there), before any hand edits:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo>            # report\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write    # write the route table\n# classify the pages in wiki-routes.json (next section)\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo>           # dry run\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo> --write   # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent (a second `convert` skips the pages it already moved). Add `--mirror <owner>/<repo>-wiki` when a repository mirrors the wiki, and `--emoji emojis.json` (from `curl -s https://api.github.com/emojis`) when pages use `:shortcodes:`.\n\n- **`routes`** lists the pages the way GitHub serves them and writes `wiki-routes.json` (page name → new route) and `wiki-files.json` (page name → file) beside `blume.config.ts`. It prints the files GitHub never served, routes that clash, digit-led routes, non-Markdown pages, and its guesses at moved notices and non-content pages. It never overwrites an existing table: it lists what differs instead.\n- **`convert`** does everything in the Navigation, Content, and Images sections below that the table and `_Sidebar.md` decide: moves each kept Markdown page to `docs/(group)/<route>.md` (`.mdx` when it needs MDX) with its frontmatter, converts wiki links, wiki URLs, alerts, fences, headings, named anchors, and duplicate link definitions, moves the wiki's images beside the pages, and writes every `meta.ts`. It prints the `navigation.featured` entries for `blume.config.ts`, the footer's text, and a `REVIEW` line for everything that needs judgment. **Every `REVIEW` line is a to-do**; the sections below say what to do with each.\n- **`check`** (after `blume build`) and **`stubs`** (for the old wiki) are in URLs and the old wiki.\n\nThe sections below are the rules the codemod applies, for checking its output and for anything you do by hand.\n\n## The page list and the route table\n\n**Which files are pages.** Every file whose extension GitHub renders: `.md`, `.markdown`, `.mdown`, `.mkdn`, `.mediawiki`, `.wiki`, `.textile`, `.rst`, `.rest`, `.org`, `.creole`, `.asciidoc`, `.adoc`, `.asc`, `.pod`, `.rdoc`. The page name is the file name without its last extension (`README.html.md.md` is the page `README.html.md`), with spaces read as `-`. Matching is case-insensitive, `-` and space are the same, and U+2010 is not `-`.\n\n- **Not pages:** `_Sidebar.*` and `_Footer.*`, in any folder (GitHub even took `_Sidebar.html.md` as a sidebar). Every other `_`-prefixed file is a page (`_ [obsolete] Config.rest`), and Blume skips `_` files by default, so the rename matters.\n- **Two files with the same name** in different folders share one URL, and GitHub serves one of them (the root one, on the wiki checked). The other was never reachable: leave it out and report it.\n- **Every other file** (images, PDFs, `LICENSE`) is served raw at `/wiki/<path>`, which redirects to `https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`.\n- **GitHub's own list** is `https://github.com/<owner>/<repo>/wiki/_pages` (every page, each linked at its URL). Compare the table with it.\n\n**Old URLs.** `Home` is `https://github.com/<owner>/<repo>/wiki` (`/wiki/Home` 301s there); every other page is `https://github.com/<owner>/<repo>/wiki/<Page-Name>`, percent-encoded.\n\n**New routes.** Each page's route is its name, lowercased, with apostrophes dropped and every other run of characters that aren't letters or digits turned into one `-` (`Standalone-Server-(tsserver)` → `/standalone-server-tsserver`, `What's-new-in-TypeScript` → `/whats-new-in-typescript`, `tsconfig.json` → `/tsconfig-json`); non-Latin letters stay. `Home` becomes `/`. Give one of two clashing pages another route by hand. A route that starts with digits and a separator (`/5-0-release-notes`) would lose them as an ordering prefix, so `convert` pins it with `slug`.\n\n**Classify every page** by editing its entry in `wiki-routes.json`:\n\n- **Content:** keep the route.\n- **A moved notice** (the whole page says it moved: \"This page has moved to <URL>\"): set its entry to that URL, or to the target page's route (with its `#anchor`) when the URL is another page of this wiki. It becomes no page; links to it go straight to the target. A short page that points elsewhere without saying it moved (\"An introduction can be found in the handbook\") is content: keep it.\n- **Not content:** a mirror repository's `README.md`, `SECURITY.md`, or `CONTRIBUTING.md`, test pages, script fixtures, and pages that only say \"deprecated\" with nowhere to point. Set the entry to `null` and list it in the report (SKILL.md step 2).\n\n`routes` prints its guesses for the moved notices and the non-content pages; confirm each one.\n\n## Config\n\nWikis have no config: map what the repository and the special pages say.\n\n| GitHub wiki | Blume |\n| --- | --- |\n| Repository name | `title` (the project's name, or \"`<Project>` wiki\") |\n| Repository description (About) | `description` |\n| The repository | `navigation.repo: \"https://github.com/<owner>/<repo>\"` for the footer link. Set `github: { owner, repo, branch, dir }` only once the pages live in a repository, so **Edit on GitHub** opens the new file. Leave it unset while the home is unknown, and report it |\n| An image at the top of `_Sidebar.md` | `logo`, with the file in `public/` |\n| External links in `_Sidebar.md` | `navigation.featured`: `convert` prints the array |\n| `_Footer.md` links | `footer.links` |\n| `_Footer.md` text | a copyright or license line → `footer.copyright`, as plain text (Markdown stripped); other text → drop (report), or a `PageFooter` layout slot (`defineComponents`). A \"contribute to this wiki\" footer drops with its link: edit links replace it |\n| \"edited this page … · N revisions\" under each title | drop (report). `lastModified: \"git\"` would show the migration date on every converted page |\n| Wiki search, the Pages list, the per-page outline, the clone URL box | built in, or drop |\n| Site URL | none existed. Ask where the site deploys and set `deployment` (SKILL.md). GitHub Pages: https://useblume.dev/guides/markdown-docs-github-pages, with `deployment: { site: \"https://<owner>.github.io\", base: \"/<repo>\" }` for a project site |\n\n## Navigation: `_Sidebar.md` → `(group)/` folders\n\nGitHub showed two things in the right rail: the **Pages** list (every page, alphabetical) and, under it, `_Sidebar.md` rendered as Markdown. The sidebar becomes folders, and every page from the Pages list stays reachable.\n\n- **Each sidebar group becomes a `(group)/` folder** (SKILL.md): it adds the sidebar group and no URL segment, so every route stays the flat one from the table. Its `meta.ts` has `title` (the label as written) and `pages` (every child in sidebar order). The content root's `meta.ts` lists the groups in sidebar order.\n- **Group labels** are whatever heads a list: a bold line (`**News**`), a heading (`## Contributing`), or an unlinked list item with children (`* Testing`, a nested `(group)/` folder with `display: \"group\"`). A heading that links a page (`# [Tips and Tricks](…/wiki/Tips-and-Tricks)`) makes that page the group's own: a real folder with the page as its `index.md`. A heading that links outside (`## [User documentation](https://…)`) is a `navigation.featured` link.\n- **A page with nested pages under it** becomes a real folder named for its route, holding the page as `index.md`, its children, and a `meta.ts` with `pages` and `display: \"group\"`. Each child gets `slug: <its route without the slash>` so its route stays flat. Without `display: \"group\"`, a nested folder lists after every loose page of its parent group, not where the sidebar had it. Blume shows the parent both as the group header and as its first row; that's expected, so don't hide it.\n- **External links** can't sit inside a generated group, so they move to `navigation.featured` (above the sidebar, on every page); report the move. A link to a heading (`[Installation](…/User-patches#installation)`) has no row in Blume: drop it (the page's outline lists it) and report it.\n- **A page listed twice** stays in its first group; report the other entry. `[[Home]]` in the sidebar drops: the home page is `docs/index.md`, listed first, outside every group.\n- **Pages the sidebar doesn't list** were still in the Pages list: they go in a last group, `(more)/` with `title: \"More pages\"`, `display: \"group\"`, `collapsed: true`, and a `pages` list sorted by title (Blume's own order goes by file name, which differs for names like `'this' in TypeScript`). Pages that shared a wiki folder can get their own collapsed group instead (`(codebase)/`); report the groups you made.\n- **No `_Sidebar.md`:** GitHub showed only the Pages list, so the pages sit at the content root (Blume sorts them by title), or in groups by the wiki's folders if it had any; report the choice.\n- **A `_Sidebar.md` inside a folder** replaced the root one for that folder's pages. Usually it's a translation (`ru/_Sidebar.md` beside `ru/Home.ru.md`): map the folder to `i18n` when the pages pair up, else to a `navigation.tabs` entry, and report it.\n- Prose, images, and HTML lists in the sidebar have no place in Blume's sidebar: move a logo to `logo`, put useful prose on the home page, and report the rest.\n\n## Content\n\n### Page files and titles\n\n- **Each kept page** lands at `docs/(group)/<route>.md`, `Home` at `docs/index.md`. It stays `.md` unless it needs MDX (GitHub alerts, mermaid, math), then it's `.mdx` (MDX hazards below).\n- **`title`** is the page name as GitHub showed it above the page: `-` → space, U+2010 → `-` (`Standalone-Server-(tsserver)` → `Standalone Server (tsserver)`, `'this'-in-TypeScript` → `'this' in TypeScript`), quoted in YAML where needed.\n- **Body H1s.** GitHub drew the name as the title and every `#` in the body below it, so a page's H1s are sections, not its title (`# TypeScript 5.3` on a page named \"API Breaking Changes\"). A leading H1 that repeats the page name (ignoring case and punctuation) is deleted. If the page still has an H1, every heading is demoted one level (`#` → `##`, `##` → `###`, …; `######` stays), and a one-line setext heading becomes ATX one level down (`===` → `##`, `---` → `###`). A setext heading over several lines has no ATX form: `convert` reports it, so rewrite it as one `###` line by hand. Heading ids come from text, so demoting changes none.\n- **`sidebar.label`** is the sidebar's text for the page, when it differs from `title` by more than case and `-` versus space.\n\n### Wiki links\n\nEvery `[[…]]` outside code converts. GitHub leaves `[[ … ]]` in fenced code alone (bash tests), and so does `convert`: it skips fenced (in a quote too) and indented code, inline code spans, `<code>` and `<tt>` elements, and HTML comments. An indented block that holds `[[` gets a `REVIEW` line, since list continuation and code look alike there. A code span that runs across lines isn't seen as code, so check any `REVIEW` line that quotes shell syntax (`[[ -f x ]]`).\n\n- **Inside an HTML block** (a line opening with `<div>`, `<p>`, `<table>`, `<details>`, and the like, up to the next blank line), Markdown shows as text, but gollum, the engine behind GitHub's wikis, resolves `[[…]]` before the Markdown is parsed, so it made a link there: `convert` writes `<a href=\"/page-name\">text</a>` there, and a bare wiki URL too. An image tag there gets a `REVIEW` line: write it as `<img src=\"…\" />` with the relative path the line gives (Blume publishes the file with the page), or move it out of the block. A `#` line inside the block is text, not a heading, so it isn't demoted.\n- **`'[[Page]]`** (an apostrophe right before, none right after) is gollum's escape for literal brackets: `convert` leaves it and reports it. Check the live page, and drop the apostrophe if it showed the brackets.\n- **`\\[\\[Page\\]\\]`** converts as a wiki link, because pandoc writes them that way (Other markup formats), with a `REVIEW` line: on a page that was always Markdown, GitHub showed those brackets, so restore them there.\n\n| GitHub (`.md` pages) | Blume |\n| --- | --- |\n| `[[Page Name]]` | `[Page Name](/page-name)`: the text as written, the route from the table |\n| `[[Link text\\|Page Name]]` | `[Link text](/page-name)` |\n| `[[Page Name#anchor]]`, `[[text\\|Page Name#anchor]]` | `[…](/page-name#anchor)`, the anchor as written |\n| `[[text\\|https://…]]` | `[text](https://…)` |\n| `[[Home]]` | `[Home](/)` |\n| a page the wiki doesn't have (GitHub drew it as a link to create the page), or one set to `null` | plain text; reported |\n| a moved notice | a link to its target |\n| `[[image.png]]`, `[[/images/x.png\\|alt=Text]]`, `[[x.png\\|align=center,frame,alt=Text]]`, `[[https://…/x.png\\|alt=Text]]` | an image: `![Text](…)` (Images and files). Any first part with an image extension is an image, whatever follows; options are separated by `,` or `\\|`. A leading `/` is the wiki root. A path the root doesn't have but the page's own folder does resolves there, reported. Options other than `alt=` (`width=`, `align=`, `frame`, stray text) drop; reported |\n| `[[text\\|docs/spec.pdf]]` (a file stored in the wiki) | left as is and reported: copy the file into `public/` and link its root path |\n| `[[_TOC_]]` | deleted: GitHub lists a table of contents among unsupported syntax, and Blume's outline shows the headings |\n\n(In this table `\\|` is the table's escape for a single pipe.) GitHub trims spaces around the pipe (`[[Mailing list | https://…]]`), and inside a Markdown table cell the pipe may be written `\\|`. Pages are looked up the way GitHub did: by name, ignoring case, with `-` and space interchangeable and folders ignored. **Anchors are case-sensitive in Blume** and weren't on GitHub, wherever they appear: when `blume validate` reports one (`#TSDeclFiles` for a heading pinned `[#TsDeclFiles]`), match the id's case.\n\n### Markdown links\n\n| Link | Blume |\n| --- | --- |\n| `https://github.com/<owner>/<repo>/wiki/Page-Name` (any case of owner and repo, `http`, `www.`), `/<owner>/<repo>/wiki/Page-Name`, or bare `Page-Name` / `./Page-Name` | the page's route through the table, keeping `#anchor`, percent-decoded (`%28` is `(`) |\n| `…/wiki`, `…/wiki/`, `…/wiki/Home` | `/` |\n| a mirror repository's copy of a page (`https://github.com/<owner>/<repo>-wiki/blob/<branch>/Page.md`) | the page's route (`--mirror`): the mirror goes stale with the wiki |\n| a bare wiki URL in prose, or `<https://…/wiki/Page>` | `[Page title](/route)` |\n| a page's file by its path (`dir/Page.md`, `…/wiki/dir/Page.md`), which GitHub served raw | the page's route; reported |\n| `#user-content-x` | `#x` (GitHub's ids carry that prefix, so both forms worked there) |\n| relative links that leave the wiki (`../issues/12`, `../blob/main/src/x.ts`, `../../other-repo`) | the absolute URL each resolved to from `https://github.com/<owner>/<repo>/wiki/<page>` (`../issues/12` → `https://github.com/<owner>/<repo>/issues/12`) |\n| a link to a page that doesn't exist (`./formatter.md`, `GLOSSARY.md#x`: GitHub served nothing there) | left as is and reported: it was broken on GitHub too, so retarget it (often the page moved to another repository) or unlink it |\n| `…/wiki/_pages`, `…/wiki/<Page>/_history`, revision URLs | left and reported: they point at the wiki's UI and stop working with it |\n| another repository's wiki | unchanged |\n\n`#123` and `@user` are plain text in wikis (GitHub doesn't autolink them there), so they stay. Bare URLs autolink in both. A raw HTML `<a href>` to a wiki page gets the route too (and a wiki link in an HTML block becomes one); Blume adds `deployment.base` to its root path like a Markdown link's.\n\n### Link definitions\n\n`blume validate` checks every link reference definition the page uses, including ones nothing cites; when a label is defined twice it checks only the first, which CommonMark uses. Wikis kept by scripts collect definitions that never rendered:\n\n- **A later definition of a label** never rendered: `convert` deletes it.\n- **A Foam or VS Code block** of autogenerated definitions (`[//begin]: # \"Autogenerated link references…\"` … `[//end]: #`) goes too: once the wiki links are converted, nothing uses it.\n- **A line like `[1]: <src/file.ts - function x(`** with no closing `>` isn't a definition at all: it shows as text, on GitHub and in Blume alike. Leave it, or delete it if it's leftover tooling syntax, and report it.\n\n### GitHub Markdown\n\n| GitHub | Blume |\n| --- | --- |\n| `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` at the start of a line | `:::note`, `:::tip`, `:::note`, `:::warning`, `:::danger`, in `.mdx`. Blume renders an alert itself as the same callout in `.mdx`, so `convert` leaves one indented in a list, or with lazy lines, as written and makes the page `.mdx`. One with text after its marker, or no body, is reported: convert it by hand. In a `.md` page an alert renders as a quote with a literal `[!NOTE]` and warns `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. The older `> **Note**` form is a plain quote on GitHub today: leave it |\n| ` ```mermaid ` | the same fence, in `.mdx` |\n| ` ```math ` | a `$$` block, in `.mdx` |\n| `$…$`, ``$`…`$``, `$$…$$` | by hand: `$$…$$` in `.mdx` (inline ones stay in their sentence; SKILL.md \"Math\") |\n| ` ```geojson `, `topojson`, `stl` | a code block: maps and 3D models drop; reported |\n| Fence languages | lowercased (`TypeScript` → `typescript`), which only tidies: Blume reads them in any case, as GitHub did. `posh` → `powershell`. Any other name Shiki lacks renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: change it to a Shiki language, or `text` |\n| `:emoji:` shortcodes | the character, with `--emoji`. GitHub slugged a heading's shortcode name, so a heading with one is reported: convert it and pin the old id (`## ⚠️ Known issues [#warning-known-issues]`) |\n| `<a name=\"x\"></a>` in a heading | deleted, with `[#x]` pinned on the heading (Blume would give the heading the id `x` anyway). Elsewhere it stays: links to `#x` work, and `blume validate` resolves them |\n| task lists, tables, footnotes, strikethrough, autolinks, `<details>` | unchanged |\n| `style=`, `<script>`, `<iframe>`, `<form>`, `<input>` | by hand: GitHub's sanitizer removed them, so readers never saw them, while Blume renders raw HTML as written. Delete them, or keep one deliberately and report it |\n\nHeading ids need no pinning beyond the rows above: GitHub and Blume both slug with `github-slugger` (identical ids for all 710 headings on the wiki pages checked). The exception is a heading that ends in an emoji or symbol after a space: GitHub keeps the trailing dash (`## Features ✨` → `#features-`), Blume drops it (`#features`); `blume validate` reports the links to fix. Deleting a leading H1 can shift the `-1` suffix of a later heading with the same text; `blume validate` reports the links that break. `pin-heading-ids.mjs` can't read a wiki anyway: GitHub puts each id on a link beside the heading, not on the heading.\n\n### MDX hazards\n\nMDX rejects things GitHub's Markdown accepts, and a page renamed to `.mdx` fails `blume build` on them. In prose (outside code), `convert` fixes the mechanical ones in the pages it renames: a `<` that doesn't start a tag (`TypeScript >=1.6, <7` → `&lt;7`), bare `{` and `}` (escaped `\\{`), HTML comments (`{/* */}` on one line), unclosed void tags (`<br>` → `<br />`), `<https://…>` autolinks, and indented code (MDX has none, so a top-level indented block gets a fence; a backslash-escaped backtick doesn't start inline code, so braces after it get escaped too). It reports the rest:\n\n- **A tag-like word** (`Array<string>` outside code, `<placeholder>`): MDX reads it as an unclosed JSX element. Put it in backticks.\n- **Raw HTML blocks** (`<details>`, `<div>`, `<table>`) with Markdown inside: MDX parses that Markdown only with blank lines around it, and fails on tags interleaved with list items. Add the blank lines, or turn the block into Markdown.\n- **Indented code inside a list:** fence it by hand, at the list item's indentation.\n\n### Other markup formats\n\nConvert each non-Markdown page to Markdown before `convert` (which skips and lists them): `pandoc -f <format> -t gfm-raw_html --wrap=preserve page.<ext> -o page.md` (`mediawiki`, `textile`, `rst`, `org`, `creole`, `asciidoc`, `pod`). `.rdoc` has no pandoc reader: convert it by hand. Delete the original, point the page's entry in `wiki-files.json` at the new `.md` file, and rerun `convert`.\n\n- **MediaWiki pages put the target first,** `[[Page Name|text]]`, as MediaWiki does. Pandoc turns them into `[text](Page_Name \"text\")`, with `_` for spaces, and `convert` maps those to routes.\n- **In the other formats** pandoc escapes `[[…]]` to `\\[\\[…\\]\\]`, and `convert` reads those as Markdown wiki links (text first).\n- Heading ids differ in these formats. Compare each converted page with the live one, and list every conversion in the report.\n\n## Frontmatter\n\nGitHub reads no frontmatter, so wiki pages have none. Each page gets `title`, plus `sidebar.label` and `slug` where the sections above say so. If a page opens with a `---` block anyway, check the live page for what readers saw, keep that as content or drop it, and report it.\n\n## Images and files\n\n- **Files stored in the wiki** (`https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`, `https://github.com/<owner>/<repo>/wiki/<path>`, or a relative `<path>`, which resolves from the wiki root whatever the page's folder) go away with the wiki. `convert` moves each referenced one into `docs/` at the same path (`docs/images/x.png`) and points the page at it relatively (`../images/x.png` from a `(group)/` page): Blume optimizes and validates relative Markdown images.\n- **A relative image that pointed at nothing** (it climbs out of the wiki root, like `../../screenshots/x.png`, or names a file the wiki doesn't have) was broken on GitHub too, and fails `blume build`: `convert` drops it and reports it.\n- **Uploaded attachments** (`user-images.githubusercontent.com`, `github.com/user-attachments/assets/…`, `private-user-images.githubusercontent.com`) are served outside the wiki and keep working for a public repository. `convert` reports each one: download and vendor it next to the pages (one request at a time) unless the user objects. From a private or internal repository they need a GitHub login, so vendoring is required.\n- **`https://github.com/<owner>/<repo>/blob/<ref>/<path>`** shown as an image is an HTML page anywhere but GitHub: `convert` uses `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`.\n- **Raw `<img>` alone on its line** becomes a Markdown image; its `width`/`height`/`align` drop and are reported. A raw `<img>` of a wiki file inside other HTML (a table cell, a `<p>`) is reported: its `src` still reads from the wiki root, which isn't the page's new folder. Copy the file into `docs/` at the same path, as `convert` does for the others, and point `src` at it relative to the page (`../images/x.png`), which Blume publishes with the page, unoptimized; or make it a Markdown image.\n- **Linked files** (PDFs, archives, an image opened as a link) are reported: copy each into `docs/` at the same path and link it relative to the page, which publishes it with the page, or into `public/` and link its root path for a URL that never changes.\n- `#gh-dark-mode-only` / `#gh-light-mode-only` pairs: keep the light image (report).\n- Files nothing references stay in the wiki (other sites may hotlink them): report them, don't copy them.\n\n## URLs and the old wiki\n\nEvery old URL is on github.com. GitHub has no redirect setting for wiki pages, and it sanitizes their HTML (no `<script>`, no `<meta>` refresh), so nothing on the old wiki can forward a reader. Don't add Blume `redirects` for them either: they would only match paths on the new site. The route table is the record instead: each old page name maps to its new route, a target URL, or `null`.\n\n**Check the table against the build** after `blume build`: `node <skill>/scripts/github-wiki-codemod.mjs check` confirms every route in the table exists in `dist/` (`dist/client/` on a server build), anchors included. Given the old URLs instead (every line of a saved `_pages` list, for one), it prints each one's new route: `ok`, `moved` for a moved notice's outside target, `DROPPED` for a page set to `null`, or `MISSING` for a page the table lacks.\n\n**What the user can do on the old wiki** (prepare it, but never commit or push it, even with push access: `stubs` only writes files, and whether and when anything goes to `<repo>.wiki.git` is the user's decision, after the new site is live):\n\n1. **Restrict editing** (Settings → Features, under Wikis: **Restrict editing to collaborators only**) as soon as the migration starts, so the content stops drifting.\n2. **Stop anything that syncs the wiki, before pushing anything to it.** Otherwise it reverts the stubs or copies them on. Look for a workflow triggered `on: gollum` (wiki edits) in the main repository, a mirror repository (`<repo>-wiki`) whose workflow pushes to `<repo>.wiki.git` on push or on a schedule, a copy of that workflow inside the wiki itself, and wiki-publishing actions. List every one you find.\n3. **Point readers at the new site:** a notice at the top of `Home` and `_Sidebar.md` (the sidebar shows on every page). In a fresh clone of the wiki (not the Blume project: `stubs` refuses to run beside `blume.config.ts`), with `wiki-routes.json` and `wiki-files.json` copied in (don't commit them there): `node <skill>/scripts/github-wiki-codemod.mjs stubs --site https://docs.example.com --notice-only --write`.\n4. **Or replace every page with a link stub,** so deep links from issues, search results, and bookmarks land one click from the new page: the same command without `--notice-only`. Each page keeps its file name, so its URL keeps working; `_Footer` files go, `_Sidebar.md` becomes the notice, images stay for sites that hotlink them, and pages set to `null` are listed for the user to decide. `--site` is the new site's URL with its base path, so regenerate the stubs once the real URL is known.\n5. **Disable the wiki** (Settings → Features → Wikis), once the repository's own links point at the new site. GitHub hides every page, stubs included, without erasing them. It's the user's call; it's never required.\n\nWhether search engines matter: `curl -sI https://github.com/<owner>/<repo>/wiki | grep -i x-robots-tag` prints `none` for a wiki GitHub keeps out of search results (GitHub indexes only wikis with 500+ stars and editing restricted to collaborators). Without it, search results keep sending readers to the old pages for a while, and the stubs are what they find.\n\n**Inbound links in the main repository.** Search its clone with `git grep -n -i -E 'github\\.com/<owner>/<repo>/wiki'`: README, CONTRIBUTING, `docs/`, `.github/ISSUE_TEMPLATE/`, `SECURITY.md`, code comments and test fixtures, `package.json` `homepage`. A large repository doesn't need a full clone: `git clone --depth 1 --filter=blob:none --sparse <url>`, then `git sparse-checkout set --no-cone '/*' '!/tests/baselines/'` (excluding its biggest generated folders). Map each URL through the table with `check`, which also verifies its anchor against the build and suggests ids for one that's gone:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs check https://github.com/<owner>/<repo>/wiki/FAQ#common-bugs\n# MISSING ANCHOR  …/wiki/FAQ#common-bugs → /faq#common-bugs (ids like: common-bugs-that-arent-bugs)\n```\n\nAn old link can carry an anchor that was already dead on GitHub: fix it to a real id, don't copy it. Rewrite each link with the new site's origin (or ask first, when the user only wants a report), and set the repository's About → Website to the new site if it pointed at the wiki. Links in issues, pull requests, and other repositories can't be rewritten: the stubs catch them. List the files you changed or would change.\n\n### Leftover check\n\nRerun until it prints only code samples you mean to keep:\n\n```bash\ngrep -rnE '\\[\\[|user-content-|(^|\\s):[a-z0-9_+][a-z0-9_+-]*:(\\s|$)' docs\ngrep -rniE 'raw\\.githubusercontent\\.com/wiki/|github\\.com/<owner>/<repo>/wiki' docs   # any case of owner and repo\n```\n\nAn alert left in a `.md` page warns `BLUME_MD_GITHUB_ALERT`, and an unknown fence language `BLUME_UNKNOWN_CODE_LANGUAGE`, so `blume build` lists those.\n\n## Teardown\n\n- **Files left behind.** Converting in place leaves the pages you didn't keep where they were: moved notices, `null` pages, the files GitHub never served, and unreferenced images. `convert` lists them. Delete them from the project (nothing under the root publishes, but they confuse later edits): the wiki keeps them, and `wiki-files.json` records their paths for the stubs. Delete `_Sidebar.md` and `_Footer.md` once `convert` has harvested them (a later `convert` then leaves the `meta.ts` files as they are).\n- **Wiki tooling** (a `package.json` for a Markdown generator, link-rewriting scripts, sync scripts and workflows) stays only if it still applies to the pages: run each script in its dry or check mode against `docs/` and see whether any page still uses the syntax it converts. If none does, it goes, with the dependencies only it used. Say which you kept.\n- **Lockfiles:** a mirror repository's `.gitignore` often ignores `package-lock.json` or `yarn.lock`. Remove those lines, so the lockfile SKILL.md step 6 regenerates gets committed.\n- Keep `wiki-routes.json` and `wiki-files.json` beside `blume.config.ts` until the stubs are pushed and the inbound links are rewritten.\n- Scaffold `package.json` (SKILL.md step 6) if the docs' new home has none.\n\n## Dropped — report these\n\nThe Pages list and the clone box; footer text other than a copyright line, and its link; sidebar prose, images, anchor links, and duplicate entries; external sidebar links moved to `navigation.featured`; the per-page \"edited … · N revisions\" line; image sizes and wiki image options; images that were broken on GitHub; maps and 3D models; theme-aware image pairs; raw HTML GitHub had stripped; links to missing pages (GitHub's create-page links) and links that were already broken; shadowed link definitions; pages left out (mirror files, tests, fixtures, deprecated stubs) and moved notices mapped to their targets; pages GitHub never served (same-name shadows); non-Markdown pages converted with pandoc; wiki-UI links (history, revisions, `_pages`); and every old URL, which stays on github.com: say what you prepared for it (notice, stubs, or neither), which sync workflows must stop first, and that nothing redirects.\n",".agents/skills/blume-migrate/references/jekyll.md":"# Jekyll (Just the Docs) → Blume\n\nJekyll is a Ruby static site generator: a `_config.yml`, Markdown pages with front matter, Liquid templating (`{% … %}`, `{{ … }}`) in every page, and Kramdown as the Markdown engine. Docs sites get their navigation from a theme, and the common docs theme is **Just the Docs** (JTD). The work is turning JTD's front-matter navigation into folders and `meta.ts` without moving a URL, and converting the Kramdown and Liquid in every page. Watch for six traps:\n\n- **Navigation lives in front matter** (`parent`, `nav_order`), not in folders. URLs come from the file path or `permalink`, so the nav tree and the file tree often disagree.\n- **Kramdown attribute lists (`{: .note }`) and Liquid (`{% include %}`, `{{ site.x }}`) render as literal text in a Blume `.md` page, with a green build.** Each warns at its line (`BLUME_TEMPLATE_TAG` for Liquid, `BLUME_MD_ATTRIBUTE_LIST` for an IAL). In `.mdx` both fail the build, and `blume check` names each line first: `BLUME_MDX_SYNTAX` for Liquid, `BLUME_MDX_ATTRIBUTE_LIST` for an IAL. Callouts are IALs, so most pages need converting.\n- **URLs end in a slash** (`permalink: pretty`, `/a/b/`) or in `.html` (the default), and relative links were written against those URLs.\n- **The page heading is the body `# H1`.** Front matter `title` is the nav label and `<title>`.\n- **Callout names are the site's own**, declared in `_config.yml`. An IAL whose name isn't declared, or is misspelled, rendered a plain paragraph.\n- **What a page shows depends on Liquid and CSS that ran around it** (`jekyll.environment`, `{% if %}`, `_data`, rules in `_sass/custom` that hide sections or add text). The built site is the record of what readers saw.\n\nScope: **Jekyll 4.x with Just the Docs** (gem or `remote_theme`) is supported, and so is a JTD site that GitHub Pages builds with its own Jekyll 3 (differences flagged below). Checked against Jekyll 4.4.1, Just the Docs 0.12.0 (navigation as of 0.10: unlimited depth, `ancestor`), kramdown 2.5.2 with kramdown-parser-gfm 1.1.0, jekyll-redirect-from 0.17.0, GitHub Pages' gem set (`github-pages` 232, Jekyll 3.10.0), and an end-to-end migration of artineering-io/flair-docs (61 pages). **Plain Jekyll and other doc themes are manual** (see the end): the codemod's Kramdown, Liquid, link, and redirect passes apply to any Jekyll site; only the navigation is JTD's.\n\n## Detect\n\n- **`_config.yml`** at the root or in `docs/` (GitHub Pages' \"deploy from `/docs`\"). It's generic Jekyll.\n- **Just the Docs:** `theme: just-the-docs` or `remote_theme: just-the-docs/just-the-docs` (also the old `pmarsceill/just-the-docs`, with or without `@ref`) in `_config.yml`, or `gem \"just-the-docs\"` in the `Gemfile`. Pages carry `nav_order`/`parent`.\n- **Not Jekyll:** a `_toc.yml` beside `_config.yml` is Jupyter Book; `hexo` in `package.json` is Hexo. A `_config.yml` holding only `theme: jekyll-theme-*` publishes a repo's README with GitHub Pages: one page.\n- **Which Jekyll:** a `Gemfile` with `gem \"jekyll\", \"~> 4…\"` is 4.x. `gem \"github-pages\"`, or no `Gemfile` with a `remote_theme`, is GitHub Pages' Jekyll 3 (3.10 today), which also forces nine plugins on (`jekyll-optional-front-matter`, `-readme-index`, `-titles-from-headings`, `-relative-links`, `-default-layout`, `-github-metadata`, `-gist`, `-paginate`, `-coffeescript`): front-matterless Markdown files are pages, `README.md` is its folder's index when there's no `index.md`, an untitled page takes its first heading as `title`, and `[x](page.md)` links work.\n\n**Where the Blume project goes:** in the Jekyll source directory (the folder holding `_config.yml`), beside `_includes/` and the pages. Set `content.root: \".\"` and scope `content.include` to the page files (`[\"*.{md,mdx}\", \"docs/**/*.{md,mdx}\"]`), and list Jekyll's own `exclude` Markdown (`README.md`, `CHANGELOG.md`) plus `vendor/**` and `node_modules/**` in `content.exclude`, which adds to the defaults `\"**/_*\"` and `\"**/.*\"` (`references/monorepo.md` §1). `_includes/` and `_data/` stay unpublished because of their underscore. In a code repo whose Jekyll source is `docs/`, that puts `package.json` in `docs/` and keeps `dist/` out of the repo root.\n\n## Build the old site first\n\nThe old build is your URL list, your nav tree, your heading ids, and the record of what rendered. Build a copy in a folder outside the repo (`<old>` below, an absolute path), with a project-local bundle. CI usually sets `JEKYLL_ENV=production`, which switches analytics and any `jekyll.environment` branches in includes. From the Jekyll source directory:\n\n```bash\nrsync -a --exclude .git --exclude _site ./ <old>/src/ && cd <old>/src\nbundle config set --local path vendor/bundle && bundle install\nJEKYLL_ENV=production bundle exec jekyll build -d <old>/site\ncd <old>/site && find . -name '*.html' -not -path './assets/*' | sed 's|^\\.||; s|/index\\.html$|/|' | sort > <old>/old-urls.txt\n```\n\nNo `Gemfile` (a GitHub Pages `remote_theme` site): write one with `gem \"github-pages\", group: :jekyll_plugins`. Recent Rubies moved `csv`, `base64`, `bigdecimal`, and `logger` out of the default gems, so under Bundler an older Jekyll fails to load them: add the one it names to the copy's `Gemfile`. If no build works, use the live site (`pin-heading-ids.mjs --old <url>`) and its `sitemap.xml`, rebuild the nav by hand, and say so.\n\nIn `old-urls.txt`, a slashed entry is a page. An `.html` entry is a page on a non-`pretty` site, `404.html`, or a jekyll-redirect-from stub, which GitHub Pages serves without the `.html` (`/old` from `old.html`): `redirects.json` in the build lists those. Non-page files Jekyll copied (`LICENSE`, `.bat` scripts, a `Rakefile`) were published by accident unless a page links them: leave them out and report them. On Jekyll 4, a `.md` file with no front matter wasn't a page either: Jekyll copied it raw.\n\n## Run the codemod\n\nFrom the Jekyll source directory, dry-run the bundled codemod against the old build, then apply it once, before any hand edits:\n\n```bash\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site .          # report only\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site --write .\n```\n\nIt's zero-dependency and deterministic, and it runs once: `--write` always writes `jekyll-migration.json` and refuses a folder that already has one, because a second pass would take each page's next H1 as its title and convert what `{% raw %}` protected. To run it again, start from a fresh copy of the Jekyll source. It reads `_config.yml` (`callouts`, `url`, `baseurl`, `permalink`, `defaults` permalinks, `exclude`, plain values for `{{ site.x }}`) and every page and Markdown partial in `_includes/`, and converts what maps exactly:\n\n- callout IALs → directives (Callouts below), `.label` → `<Badge>`, `.no_toc` and `{#id}` on headings → `[!toc]` and `[#id]`, and JTD's utility and button classes dropped; a malformed IAL, or one naming a class `callouts:` doesn't declare, rendered a plain block, so it goes and is reported;\n- the in-page TOC (the list, its `<details>`, its heading, a partial holding only a TOC), `{::comment}`, ALDs;\n- the body H1 → `title` (a different old `title` → `sidebar.label` and `seo.title`), with a variable it showed written in as its value, since Blume leaves front matter as written; an H1 inside an HTML comment or `<pre>` is skipped. The front matter keys follow the table below;\n- `{% include x.md k=\"v\" %}` and `{% include_relative %}` → `<include k=\"v\">/_includes/x.md</include>` (nested ones too, `{{ include.k }}` → `{{k}}`); a parameter the partial reads but the tag leaves out printed nothing in Jekyll, so it's passed as `k=\"\"`. Also `{{ site.x }}` → `{{x}}` variables, `{{ site.baseurl }}`, `relative_url`, `absolute_url`, `{% link %}`, `{% highlight %}`, `{% comment %}`, and `{% raw %}` (unwrapped, its content untouched);\n- links: relative links resolved against each page's old URL, `.md`, `.html`, own-origin, and trailing-slash links → routes; relative Markdown images rebased from the page's new folder, and a relative raw HTML `src` → a root path (Jekyll resolved it against the page's old URL, which the new folders don't mirror);\n- with `--old`, the sidebar the old build rendered → folders, `slug`s, and `meta.ts` files (Navigation below);\n- each page that now needs MDX → `.mdx`, with void tags self-closed, comments as `{/* */}`, a bare `<` as `&lt;`, autolinks as links, and page `<script>`/`<style>` removed, inline code left as written; partials such a page includes get the same;\n- `redirect_from`, `redirect_to`, `.html` URLs, and the variables → `jekyll-migration.json`, for the config:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport migration from \"./jekyll-migration.json\" with { type: \"json\" };\n\nexport default defineConfig({\n  redirects: migration.redirects,\n  variables: migration.variables,\n});\n```\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each: HTML includes, Liquid logic, unknown front matter, MDX hazards (a brace in prose, an indented code block), the CSS rules that hid or added content, and the theme files. A `{{ name }}` that `{% raw %}` kept as written collides with a variable of the same name, which Blume fills in everywhere, code included: rename the variable (its key in `jekyll-migration.json` and the `{{name}}` references the codemod wrote). A site whose `_config.yml` has no `callouts:` (before JTD 0.4 added the option, sites styled `.note`/`.warning` in `_sass/custom/custom.scss`) keeps its callout IALs, and the dry run says so: read those rules for the names, then pass `--callouts note,warning` on the write run.\n\nAfter the first `blume build`, pin the old heading ids, rebuilding until it reports 0. Jekyll's GFM ids drop underscores (`f_position` → `#fposition`) where Blume keeps them, so about one id in twenty differs:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old>/site --write\n```\n\n## Config: `_config.yml` → `blume.config.ts`\n\nApply `defaults:` (front matter defaults) to the pages in each `scope.path` that the codemod didn't see through: they often set `layout`, and sometimes `parent` or `nav_exclude`.\n\n| Jekyll | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `url` | `deployment.site`, unless the target is Vercel or Netlify (SKILL.md). GitHub Pages with a custom domain needs it |\n| `baseurl` (`/repo`) | `deployment: { base: \"/repo\" }`. **The deployed URL decides:** the GitHub Pages starter workflow passes `--baseurl` from the Pages settings, so a custom domain (a `CNAME`) serves at the root whatever `_config.yml` says |\n| `exclude` / `include` | `content.exclude` / `content.include` (Detect) |\n| `permalink`, `defaults`, `collections` | URLs; above; Collections and posts |\n| `plugins` | Plugins |\n| `kramdown.syntax_highlighter_opts.block.line_numbers: true` | no site-wide switch: drop it and report it. Code without line numbers copies cleanly |\n| `kramdown.toc_levels`, and pages built on H4 sections | Blume's outline shows H2–H3, where Kramdown's TOC listed every level. Count the H4s: when they carry the page (attribute references), set `toc: { maxHeadingLevel: 4 }`; else report it |\n| other `markdown`, `kramdown`, `highlighter`, `sass`, `compress_html`, `liquid` keys | drop |\n| `github_username`, `twitter`, `social.links`, a Discord link | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube) |\n| `repository` (jekyll-github-metadata) | `github: { owner, repo }` |\n| `logo` | `logo`, file moved to `public/`. JTD shows the logo _instead of_ the title, so a wordmark needs `logo: { image, text: \"\" }`; a custom `_includes/title.html` that shows both keeps the text |\n| `favicon_ico` | the file at `public/favicon.ico` (no config field) |\n| `color_scheme: dark` | `theme.mode: \"dark\"`. `light`/unset → omit. A custom scheme (`_sass/color_schemes/<name>.scss`): its `$link-color` → `theme.accent` (`$blue-000` is `#2c84fa`; look the rest up in the theme's `_sass/support/_variables.scss`), and an `@import \"./color_schemes/dark\"` in it → `theme.mode: \"dark\"` |\n| `aux_links` (`\"Label\": \"url\"` or `[\"url\"]`) | `navigation.actions: [{ label, href }]`. `aux_links_new_tab` → drop (Blume opens `http(s)` header links in a new tab) |\n| `nav_external_links` (`title`, `url`) | `navigation.featured` (above the sidebar, where JTD listed them below the pages) |\n| `search_enabled: false` | `search: false`. `search.*` options → drop |\n| `nav_enabled: false` | no site-wide switch: `mode: center` on the pages that showed no sidebar |\n| `mermaid` | delete; ` ```mermaid ` renders natively in `.mdx` |\n| `ga_tracking` | `googleAnalytics({ id })` from `blume/analytics`, one per `G-` id in the comma list. A `UA-` id no longer collects data: report it |\n| `gh_edit_link` + `gh_edit_repository` + `gh_edit_branch` | `github: { owner, repo, branch }`. `gh_edit_source: docs` → `github.dir: \"docs\"` when `blume.config.ts` sits there |\n| `last_edit_timestamp` | `lastModified: \"frontmatter\"` when pages set `last_modified_date` (the codemod maps it), else drop: JTD showed no date without it |\n| `footer_content` / `_includes/footer_custom.html` | links → `footer.links`, social links → `footer.socials`; copyright text → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), and turn a Liquid year (`{{ \"now\" \\| date: \"%Y\" }}`) into `${new Date().getFullYear()}` in a template literal; other text → drop (report) or a `Footer` layout slot (`defineComponents`) |\n| `callouts`, `heading_anchors`, `enable_copy_code_button`, `back_to_top*`, `nav_sort`, `nav_error_report`, `ga_tracking_anonymize_ip`, `gh_edit_link_text`, `gh_edit_view_mode` | drop (the codemod read `callouts`) |\n\n## Navigation: front matter → folders and `meta.ts`\n\nJTD builds one tree from `title`, `parent`, `grand_parent`/`ancestor`, and `nav_order`, wherever the files sit, and shows it as a collapsible tree with only the current section open: set **`navigation.sidebar.display: \"group\"`**. The codemod rebuilds the tree from the sidebar the old build rendered, so `nav_order` ties, `child_nav_order: reversed`, and `nav_sort` are already resolved, and keeps every URL:\n\n- A parent at `dir/index.md` with its children in `dir/` (the layout JTD's docs recommend) moves nothing.\n- A parent that's a leaf file with its children beside it (`docs/setup.md`, `docs/install.md`) becomes a `(group)` folder: `docs/(setup)/index.md` with `slug: docs/setup`, and the children inside it keep their URLs.\n- A child whose file sits elsewhere moves under its parent with `slug: <old route>`. A child _section_ whose folder sits elsewhere is reported: `git mv` it into the parent's folder (into a `(group)` folder, its URLs stay; into a real one, pin each page's `slug`), or leave it. The parent's `meta.ts` already lists it by its folder name.\n- **A `parent` title that names two pages** (two sections both titled \"Setup\", told apart by neither `grand_parent` nor `ancestor`): JTD listed the child under both. Blume lists a page once, so it stays under the first and the codemod reports it.\n- Each folder gets a `meta.ts`: `pages` in the old order; `title` only where Blume's label for the folder (its name, humanized) differs from JTD's; `directory: \"accordion\"` where the old parent page listed its children (JTD's `has_toc`, read from the old build), and `\"none\"` below it where a page didn't, since `directory` inherits.\n- **The duplicate index row:** Blume links a folder's `index` from the group row and lists it again as the first row, where JTD showed it once. The codemod hides that row (`sidebar.hidden: true`); a page the group row links stays in the sitemap, search, and `llms.txt`. `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`, flags a hidden index whose `title` differs from its folder's `meta.ts` `title`, unless its `sidebar.label` names the folder title. An index whose H1 differed from its JTD title gets that title as `sidebar.label`, which is the section label, so it passes too. Where neither the page's `title` nor its `sidebar.label` matches the section label (JTD always labeled a section with its page's `title`, so this is rare), the codemod leaves the row visible and says so. The check accepts three ways out: match the two titles, set the page's `sidebar.label` to the folder title, or show the row (remove `sidebar.hidden`).\n- Pages outside the old sidebar (`nav_exclude`, no `title`, a `parent` that matched nothing) get `sidebar.hidden: true`, which also takes them out of search, the sitemap, and `llms.txt`, where JTD still searched them: set `search: { indexing: { includeHiddenPages: true } }` if they should stay searchable. A hidden home page is the exception: the root URL serves it, so it stays listed. An external `nav_external_links` entry is listed for `navigation.featured`.\n\nWhat's left for you:\n\n- **A wrapper folder JTD never showed** (the template's `docs/`, any folder whose pages sit at the nav's top level) becomes one group around them. Blume opens a group that's the sidebar's only top-level row by itself, but one beside the home page's row or another section starts closed off its own pages, so the codemod writes `collapsed: false` in its `meta.ts`. A `navigation.tabs` entry instead lifts its pages to the sidebar's top level, but there pages list above groups, so JTD's interleaved order breaks. Keep the group unless the user asks for a tab.\n- **The home page** (`index.md`, often `layout: home` and untitled) stays the content root's `index`. When the old nav never showed it, the codemod hides its row like any page outside the nav; a hidden home page stays in the sitemap, search, and `llms.txt`. Give it a `title` (Blume renders it as the H1, where JTD showed none) or `mode: frame` to keep the sidebar without one. Where the home page headed a JTD section, Blume's root index can't head a group: its children stay in their folders, and the codemod reports it.\n- **Moving a page changes what its relative images resolve against.** The codemod rebases the Markdown images it converted, and turns a relative raw HTML `src` into a root path, with a REVIEW to serve that file from `public/`.\n\n## URLs and redirects\n\nA Jekyll page's URL, for `dir/name.md`:\n\n| `permalink` (global) | `dir/name.md` | `dir/index.md` | Blume route |\n| --- | --- | --- | --- |\n| `pretty` | `/dir/name/` | `/dir/` | `/dir/name`, `/dir` |\n| unset (`date`), `none`, `ordinal` | `/dir/name.html` | `/dir/` | same |\n| a style ending in `/` / in `:output_ext` / neither | `/dir/name/` / `.html` / `/dir/name` (file `name.html`) | `/dir/` | same |\n\nA page's own `permalink:` wins, then one `defaults:` sets for its folder (the most specific `scope.path`), then the global style. The codemod applies all three, and with `--old` it checks each URL against the build: a page the build doesn't have at that URL gets a REVIEW and no `.html` redirect. Blume serves `<route>/index.html`, so `/dir/name/` keeps working on static hosts, and `blume dev` and `preview` redirect it to `/dir/name`.\n\n- **`.html` URLs** become one exact redirect each (a pattern can't strip a suffix). Never add one from `/dir/`, `/dir/index.html`, or `/index.html` (SKILL.md, Redirects).\n- **`redirect_from`** values become redirects as written (`/old` and `/old/` are different `from`s), keeping case (`/w/Main_Page`); one that is the page's own URL is skipped. **`redirect_to`** becomes a redirect from the page's route, and an empty page with it is deleted.\n- **Raw HTML** (`<a href>`, `<img src>`, `<video><source src>`) gains `deployment.base` on a root path, as a Markdown link does, so the root paths the codemod writes from relative links, `{{ site.baseurl }}`, `relative_url`, and `{% link %}` need nothing more. `blume validate` checks a raw `<a href>` against the routes and a media `src` against `public/`.\n- **Jekyll 3's `{% link %}`** doesn't add `baseurl` (Jekyll 4's does, so `{{ site.baseurl }}{% link … %}` doubled it there). The codemod drops both: Blume adds the base itself.\n\n## Content: what the codemod leaves\n\n**HTML includes** (`{% include x.html … %}`) are never spliced: `<include>` embeds a non-Markdown file as code. Replace each use and delete the partial when nothing uses it:\n\n- a figure → a Markdown image, or `<Frame caption=\"…\">` around it;\n- a YouTube iframe → `<YouTube url=\"…\" />`, inside `<Frame caption>` when the include printed a caption; a `width` → drop (report). A playlist URL (`/embed/videoseries?list=…`) works as written;\n- a row of embeds or code blocks in a flex `<div>` → `<Columns cols={2}>`;\n- a jQuery UI accordion (`$(\".accordion\").accordion()` over alternating question lines and answer `<div>`s) → `<Accordion>` with one `<AccordionItem title>` per question, the first `defaultOpen` as jQuery UI opened it; an `<details>` with Markdown in it → `<Expandable title>` (MDX wraps a `<summary>` and the text after it in one paragraph);\n- a note include (`note.html`, `callout.html`) → a directive.\n\n**Liquid logic** (`{% if %}`, `{% for %}`, `{% assign %}`, `{% capture %}`, a dynamic `{% include {{ var }} %}`, a parameter passed as a variable) → the output the old build shows: split a partial with `{% if include.x %}` into one partial per variant, write a `_data` loop's output once as Markdown, inline `{{ page.x }}`. A `{{ site.x }}` that isn't a plain value in `_config.yml` is left too. Liquid ran inside fenced code as well, unless wrapped in `{% raw %}`, so a REVIEW in a code block means the old page showed the rendered value.\n\n**CSS that changed content.** The codemod lists every `display: none` and `content: \"…\"` rule in `_sass/` and `assets/css/`. Find what each targets in the pages: content a rule hid wasn't shown, so delete it (and report the anchors that go with it); text a `::before`/`::after` added (`.label::before { content: \"New: \" }`) belongs in the converted element's text.\n\n**MDX hazards** the codemod lists in `.mdx` pages: a brace in prose (escape it `\\{` or put it in backticks), an indented code block (fence it), anything else the build names with a line. A partial with a hazard fails the build at the partial's own line (`blume check` warns `BLUME_MDX_SYNTAX` there first), and an unclosed `<img>` in a partial doesn't fail at all: it pulls the rest of the partial inside its parent element. The codemod self-closes the ones it sees, and `BLUME_MDX_UNCLOSED_ELEMENT` names the partial's line for any left.\n\n**A title in a partial:** a page that's only `{% include api/x.md %}` takes its heading from the partial's H1. Move it into the page's `title` (plain text: no `<kbd>` or backticks) and out of the partial, or Blume renders two.\n\n**Also by hand:** definition lists (`Term` + `: Definition` → a list or `<dl>`), `*[ABBR]:` abbreviations (delete), Font Awesome `<i class=\"fa…\">` icons (`<Icon icon>` with a Lucide name, or drop), and page scripts the codemod removed (an island, a `PageFooter` slot, or static content: two images instead of a before/after slider).\n\n### Callouts\n\nJTD has no built-in callout names: each site declares its own under `callouts:` with a `color` and an optional `title`, which JTD printed uppercase above every callout of that name. The codemod maps by the name's meaning, then by color:\n\n| JTD name (template color) | Blume |\n| --- | --- |\n| `note` (purple), `important` (blue) | `:::note`, `:::important` (an alias of note) |\n| `tip`, `hint` | `:::tip` |\n| `info`, `highlight` (yellow, untitled) | `:::info` |\n| `warning`, `attention`; `caution` | `:::warning`; `:::caution` (an alias of warning) |\n| `danger`; `error` | `:::danger`; `:::error` (an alias of danger) |\n| `new`, `success` (green) | `:::success` |\n| any other name | by its color: blue → `info`, green → `success`, yellow → `warning`, red → `danger`, purple or grey → `note` |\n\nBlume shows a type's icon, not its name, so a configured `title` that only names the directive is dropped and one that says more is kept (`new` titled \"New\" → `:::success[New]`). `{: .note-title }` takes its title from the blockquote's first paragraph. A callout class on a list, table, or code block rendered no box in JTD (it styles only paragraphs and blockquotes), so it's dropped. The codemod applies each IAL to the block Kramdown did: a list or blockquote that follows a paragraph without a blank line is its own block, and an IAL with blank lines on both sides applies to the next block. An IAL indented under a list item sat inside the item, where Kramdown sometimes boxed the item's paragraph: those are reported, so check the old page and write the callout inside the item by hand.\n\n## Front matter\n\nThe codemod maps these, and reports every other key Blume's strict schema rejects:\n\n| Jekyll / JTD | Blume |\n| --- | --- |\n| `title` + body H1 | `title` = the H1; a different old title → `sidebar.label` and `seo.title` |\n| `description`, or `summary` (JTD showed it in child lists) | `description`. JTD only put it in `<meta>`; Blume shows it under the title (report) |\n| `layout: default` / `page` / `home` / `post` / `about` | removed |\n| `layout: minimal`, `nav_enabled: false` | `mode: center` |\n| a custom `layout` from `_layouts/` | reported: pick a `mode` or a custom `.astro` page |\n| JTD nav keys, `permalink`, `redirect_from`, `redirect_to` | removed (Navigation, URLs) |\n| `search_exclude: true` | `search: { exclude: true }` |\n| `last_modified_date` | `lastModified` (ISO date) |\n| `published: false` | `draft: true` |\n| `image` (a string, or `{ path }`), `canonical_url` | `seo.image`, `seo.canonical` |\n| jekyll-seo-tag's `seo:` `type`/`name`/`links` | removed |\n| `sitemap: false` | reported: `seo: { noindex: true }` if it shouldn't be indexed, else delete |\n| `author`, `date`, `lang`, `tags`, `categories`, `excerpt`, `toc` | reported: delete (`tags` → `search.tags` if a hosted search filters on them) |\n\n## Theme, layouts, and assets\n\n- **`_sass/custom/custom.scss`** → a project-root `theme.css` with only the rules whose markup still exists (figure classes, flex rows): JTD selectors (`.main-content`, `.site-nav`, `.site-title`) match nothing in Blume. Turn hard-coded colors into Blume's tokens.\n- **Theme hook includes** (the codemod lists them): `head_custom.html` (favicons → `public/`, `<meta>` → `seo.metatags`, analytics → adapters, icon-font CSS → drop), `header_custom.html` (an announcement → `banner`), `footer_custom.html` (Config), `title.html` (→ `logo`), `nav_footer_custom.html`, `search_placeholder_custom.html`, `toc_heading_custom.html`, `mermaid_config.js`, `js/custom.js`, `lunr/*` → drop and report what each did.\n- **`_layouts/`** and `.html` pages with front matter → `.mdx` (with `mode: custom` for a landing page) or a custom `.astro` page. `404.html`/`404.md` → delete (Blume ships one).\n- **Assets:** a folder the old site served by root path (`/media/…`, `/assets/images/…`) moves whole to `public/` at the same path. Keep files no page references: other sites may link them. List them, and prune only once the user confirms. JTD's own `assets/css` and `assets/js` go.\n- **Root files** (`favicon.ico`, `apple-touch-icon.png`, `site.webmanifest`, `.well-known/`) → `public/`. A `robots.txt` replaces Blume's generated one: keep it only if it says something Blume's doesn't. `CNAME` and `.nojekyll` aren't needed with a GitHub Actions deploy.\n\n## Collections and posts\n\n- **A JTD collection** (`collections: { docs: { output: true, permalink: \"/:collection/:path/\" } }` plus `just_the_docs.collections.docs.name`): rename `_docs/` → `docs/` so the URLs stay `/docs/…`, then run the codemod; give the folder a `meta.ts` `title` from `name`. `nav_fold: true` → `collapsed: true`. A collection document without `title` took its filename as one: write it in.\n- **`_posts/`** → `type: blog` pages (`docs/advanced/blog.mdx`), with `slug` pinned to the old dated URL (`/:categories/:year/:month/:day/:title.html` by default, so also an `.html` redirect) or redirected; `{% post_url %}` → the post's route; jekyll-feed's `/feed.xml` → a redirect to `/blog/rss.xml`.\n\n## Plugins\n\n| Plugin | Blume |\n| --- | --- |\n| `jekyll-redirect-from` | `redirects` (the codemod) |\n| `jekyll-seo-tag`, `jekyll-sitemap` | built in (the sitemap needs `deployment.site`) |\n| `jekyll-relative-links` | built in: the codemod turns `.md` links into routes |\n| `jekyll-include-cache` (`include_cached`) | same as `include` |\n| `jekyll-remote-theme`, `jekyll-default-layout`, `jekyll-optional-front-matter`, `jekyll-titles-from-headings`, `jekyll-readme-index` | delete; the old build shows the pages and titles they made |\n| `jekyll-github-metadata` (`site.github.*`) | inline the values; `repository` → `github` |\n| `jekyll-last-modified-at` | `lastModified: \"git\"` |\n| `jemoji` (`:smile:`), `jekyll-mentions` (`@user`), `jekyll-gist` | paste the emoji; write the links; a link or a fence |\n| `jekyll-paginate`, `jekyll-coffeescript`, `jekyll-sass-converter`, `jekyll-compose` | drop |\n| anything else (`_plugins/*.rb` included) | read what it does; report it |\n\n## Plain Jekyll and other themes: manual\n\nRun the codemod without `--old` (or with it, for the URLs): its content and link passes apply to any Jekyll site. The navigation source differs, and there's no tested mapping for it:\n\n- **Plain Jekyll / minima:** `header_pages` (a list of files) or no nav. Lay out folders by topic and report the new sidebar.\n- **Minimal Mistakes** (docs layout): `_data/navigation.yml` holds the sidebar (a key such as `docs:` listing `title` + `children: [{ title, url }]`), and pages opt in with `sidebar: { nav: \"docs\" }`. Convert it like a config-declared nav (SKILL.md). Notices `{: .notice }` and `.notice--primary`/`--info`/`--warning`/`--success`/`--danger` → pass those names with `--callouts` and map the types by hand; `toc: true` and `toc_*` → delete.\n- **Documentation Theme for Jekyll:** `_data/sidebars/*.yml` (`entries` → `folders` → `folderitems`, then `subfolders` → `subfolderitems`), with `sidebar: <file>` and a flat `permalink: name.html` on every page. `{% include note.html content=\"…\" %}` (also `tip`, `warning`, `important`, and `callout.html` with a `type`) → directives; `summary` → `description`; `keywords` → `search.keywords`; `last_updated` → `lastModified`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts` (a Ruby site has none: scaffold it), Node 22.19+, `.gitignore` for `node_modules/`, `.blume/`, `dist/`, `.env.local`.\n- **CI:** replace `bundle exec jekyll build` and the Pages starter workflow with the Blume one (https://useblume.dev/guides/markdown-docs-github-pages). Its `npm ci` needs a lockfile, so commit the `package-lock.json` from `npm install` in the same change. A branch-deployed Pages site moves its source to GitHub Actions in the repo settings, where the custom domain also stays: list both as manual steps. Dependabot's `bundler` entry → `npm`. Scripts that wrapped Jekyll (`serve.bat`, `Rakefile`) → delete or repoint.\n- **Delete** `_config.yml`, `Gemfile`, `Gemfile.lock`, `_layouts/`, `_sass/`, theme hook and HTML includes, JTD's `assets/` files, `_site/`, `.jekyll-cache/`, and `vendor/bundle/`, once everything they declared is harvested. Keep `_includes/` partials pages still `<include>`, and `jekyll-migration.json`.\n- **Grep the whole repo** for moved page paths and for `jekyll` (CODEOWNERS, link checkers, a README \"build the docs\" section).\n\n## Leftover check\n\nRerun until it prints only code samples, `{{name}}` variables and include props you defined, and relative links you meant:\n\n```bash\ngrep -rnE '\\{:[ .#a-z:]|\\{%|\\{\\{|^\\*\\[|^: |markdown=\"|<i class=\"fa|\\.html[)#\"]|\\]\\(\\.\\.?/[^).]*\\)|\\]\\(/[^)]*/(#[^)]*)?\\)' \\\n  --include='*.md' --include='*.mdx' --exclude-dir={node_modules,vendor,_site,.blume,dist} .\n```\n\n## Dropped — report these\n\nCallout title labels that only named the type, unconfigured or malformed callout IALs left plain, the button and label styling the codemod dropped, `nav_external_links` moved above the sidebar, `back_to_top`, other `footer_content` text and links inside the copyright, theme hooks, custom color schemes beyond the accent, `_sass` rules with no Blume target, content a CSS rule hid, site-wide code line numbers, deeper TOC levels, `UA-` analytics ids, `sitemap: false`, abbreviations, definition-list styling, Liquid logic and `_data` loops now frozen as content, embed widths, page scripts and widgets (sliders, accordions) not rebuilt, icon-font icons with no Lucide match, accidentally published files (`LICENSE`, `.bat`), and every plugin removed without an equivalent.\n",".agents/skills/blume-migrate/references/mdbook.md":"# mdBook → Blume\n\nmdBook is the Rust project's book generator: a `book.toml`, a hand-written `SUMMARY.md` that is the whole table of contents, and chapters in `src/` rendered by pulldown-cmark after preprocessors rewrite them (the built-in `links` expands `{{#include}}`; plugins like mdbook-admonish and mdbook-mermaid add the rest). Watch for five traps:\n\n- **Every URL ends in `.html`** (`src/guide/setup.md` → `/guide/setup.html`, a `README.md` → `index.html`), so every chapter that isn't an index needs an exact redirect.\n- **`SUMMARY.md` is the only navigation, and its nesting rarely exists on disk.** A parent chapter is a page _and_ a group, and its file sits beside its children's folder (`guide.md` + `guide/`) or flat next to them.\n- **`{{#include}}` pulls code from anywhere in the repo, by `ANCHOR` name or line range.** Blume's `<include>` takes whole files from inside the content root only, so code the crate compiles goes through generated excerpts (Includes). Anchors that no longer exist rendered as empty blocks on the old site, with no warning, so expect several.\n- **Fences carry Rust-only syntax.** Hidden `# ` lines and attributes like `rust,ignore` show up as code or kill highlighting if left alone.\n- **The page heading is the body H1, but the sidebar label and `<title>` are the `SUMMARY.md` text**, and the two often differ.\n\nScope: mdBook 0.4.x and 0.5.x, checked against 0.4.48, 0.4.52, and 0.5.4, mdbook-admonish 1.19.0, mdbook-mermaid 0.15.0, and an end-to-end migration of the Linera manual (39 chapters, mdBook 0.4.48). The syntax of mdbook-katex, -toc, -tabs, and -alerts comes from their docs; it wasn't run.\n\n## Detect\n\n- **`book.toml`**, with `[book]` and usually `[output.html]` tables: only mdBook uses it. It sits at a standalone book's root or in a folder of a code repo (`docs/`, `book/`, `doc/`, `guide/`, `mdbook/`). The chapters live in `[book] src` (default `src/`), which can point outside that folder (`src = \"../docs\"`).\n- **`SUMMARY.md`** in that `src`. GitBook and HonKit use `SUMMARY.md` too, but with `.gitbook.yaml` or `book.json`, never `book.toml`.\n- CI that runs `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, or `cargo install mdbook mdbook-<plugin>`. The workflow that deploys may live in another repo (one that checks the code repo out as a submodule).\n- **Fingerprint the version** from the CI pin. 0.5 removed `curly-quotes`, `multilingual`, `google-analytics`, and `copy-fonts`, rejects unknown `book.toml` keys, and fails on plugins built for 0.4 (mdbook-admonish 1.19.0 and mdbook-mermaid 0.15.0 both do), so most books with plugins still build with 0.4.x.\n\n**Where the Blume project goes.** Beside `book.toml`, the folder `mdbook build` ran in, with `content.root` set to the book's `src` (Blume's default is `docs`, so set it unless `src = \"docs\"`). That keeps `package.json`, `public/`, and `dist/` out of a Rust repo's root. mdBook has no manifest: scaffold `package.json` there (SKILL.md step 6). **A `src` outside that folder (`\"../docs\"`) works**, edit links included, and a `meta.ts` there imports `defineMeta` from `blume` as usual. That import needs a Blume release after 2.1.3: earlier ones resolved `blume` only from the meta file's own folder up, so it failed with `BLUME_META_LOAD_FAILED … Cannot find module 'blume'`. On those, export a plain object instead (`export default { title: \"Developers\", display: \"flat\", pages: […] };`).\n\n## Build the old book first\n\nBefore changing anything, build the book with the version CI pins and its plugins, into a folder outside the repo. Prebuilt binaries of mdBook and most plugins are on their GitHub releases pages (mdbook-admonish has none for Apple Silicon: `cargo install --root <scratch> mdbook-admonish@<version>`); install them in a scratch folder, not globally.\n\n```bash\nmdbook build <book-root> -d /abs/path/old-book\nMDBOOK_OUTPUT='{\"markdown\": {}}' mdbook build <book-root> -d /abs/path/old-md\n(cd /abs/path/old-book && find . -name '*.html' ! -name print.html ! -name toc.html ! -name 404.html | sed 's|^\\.||' | sort) > old-urls.txt\n```\n\n- `old-book` is the old site: its `.html` files are the old URLs, its headings carry the old ids, and its pages show what readers saw. With two or more `[output.*]` tables (`[output.linkcheck]` counts), the HTML lands in `old-book/html/`: use that folder wherever this file says `old-book`.\n- `old-md` is each chapter as Markdown after `{{#include}}` and the other helpers ran (admonish blocks stay as written): what every include actually showed.\n- Under 0.4 a missing plugin only warns, and its blocks render as code; the URLs are still right. Under 0.5 it fails unless `optional = true`.\n\n## Config: `book.toml` → `blume.config.ts`\n\nMap only what's set. CI can override keys with `MDBOOK_BOOK__…`/`MDBOOK_OUTPUT__…` environment variables, so read those too.\n\n| `book.toml` | Blume |\n| --- | --- |\n| `[book] title` | `title`. Unset, mdBook showed no title: take the first chapter's H1, or ask |\n| `description` | `description` |\n| `src` | `content.root` |\n| `language` (not `en`) | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`); `text-direction = \"rtl\"` → that locale's `dir: \"rtl\"` |\n| `authors`, `multilingual`, `[rust] edition`, `[build] build-dir`, `create-missing`, `extra-watch-dirs` | drop. With `create-missing` (on by default) mdBook created an empty page for a `SUMMARY.md` entry whose file was missing: check the old build |\n| `use-default-preprocessors = false` | without `[preprocessor.links]`/`[preprocessor.index]`, `{{#…}}` stayed literal and `README.md` stayed `README.html`: read the old build |\n| `[output.html] site-url = \"/repo/\"` | `deployment: { base: \"/repo\" }`. It's a path, not an origin |\n| `cname` | `deployment: { site: \"https://<cname>\" }`; GitHub Pages isn't auto-detected. A project site with no domain → `{ site: \"https://<owner>.github.io\", base: \"/<repo>\" }` |\n| `git-repository-url` on GitHub | `github: { owner, repo }`. Elsewhere → `navigation.actions` (report) |\n| `edit-url-template` | `github.branch` from the segment after `edit/` or `blob/`. Set `github.dir` to the Blume project's path in the repo (`git rev-parse --show-prefix`), not the template's prefix: mdBook fills `{path}` with `<src>/<file>`, so a `src` outside the book root gave the old site broken edit links (`…/edit/main/../docs/x.md`) |\n| `default-theme`, `preferred-dark-theme` | mdBook switches between them on the reader's system preference. Both dark (`coal`, `navy`, `ayu`) → `theme.mode: \"dark\"`; a light one plus a dark one → omit. A theme switcher deleted from `theme/index.hbs`, or a stylesheet that repaints the default theme, forces one look: read the CSS, not the name |\n| `additional-css` | a root `theme.css` with only the rules whose markup still exists (`.content`, `.sidebar`, `.chapter`, `--links`, `--sidebar-*` match nothing). Read brand colors and fonts into `theme.accent`, `theme.background`, and `theme.fonts`; Blume already defaults to Inter and IBM Plex Mono. Delete `mdbook-admonish.css` |\n| `additional-js` | delete the mermaid, admonish, and KaTeX assets. A gtag loader → `googleAnalytics({ id })` from `blume/analytics`; other scripts → `script()` or report |\n| a `theme/` folder | diff it against `mdbook init --theme` from the same version first: stock files carry nothing to port, and the stock `favicon.png`/`favicon.svg` are mdBook's own logo (report, don't copy). Custom ones: `<head>` meta tags → `seo.metatags`; analytics → adapters; a favicon → `public/`; fonts → `theme.fonts`; the rest drops (report) |\n| `google-analytics` (0.4) | `googleAnalytics({ id })` for a `G-` id; report a `UA-` id |\n| `mathjax-support` | Math (Other syntax) |\n| `smart-punctuation` / `curly-quotes` | Blume always converts quotes and dashes. Where the old site had it off (0.4's default), `--flag` in prose becomes an en dash: put flags in backticks |\n| `no-section-label` | nothing. Without it, mdBook numbered the chapters (`1.`, `2.3.`) in the sidebar, and Blume can't: report it, and fix prose that cites \"chapter 4\" |\n| `input-404`, `src/404.md` | delete the page; Blume ships a 404 (`pages/404.astro` to customize) |\n| `[output.html.search]` | built in: drop. `search.chapter.\"<path>\" = { enable = false }` → `search: { exclude: true }` on those pages |\n| `[output.html.fold]` | Navigation |\n| `[output.html.redirect]` | URLs and redirects |\n| `[output.html.playground]`, `[output.html.code.hidelines]` | Code blocks |\n| `[output.html.print]`, `hash-files`, `copy-fonts`, `sidebar-header-nav`, `git-repository-icon`, `definition-lists`, `admonitions`, `live-reload-endpoint`, `[output.markdown]` | drop |\n| `[output.linkcheck]`, `[output.linkcheck2]` | `blume validate --strict` (`--external` for web links) |\n| other backends (EPUB, PDF) | `export: { pdf: true }` prints one page at a time; a whole-book PDF or EPUB has no equivalent (report) |\n\n## Preprocessors\n\n| `[preprocessor.X]` | Blume |\n| --- | --- |\n| `links` (built in) | Includes |\n| `index` (built in) | rename every `README.md` chapter to `index.md` |\n| `admonish` | Admonitions |\n| `mermaid` | ` ```mermaid ` works as written in `.mdx`; delete `mermaid.min.js` and `mermaid-init.js` (theme settings in `mermaid-init.js` drop) |\n| `katex` | `$…$` → `$$…$$` inside its sentence; `$$` blocks stay; `\\$` → `$`. Custom delimiters → convert from those. A `macros` file has no equivalent: expand the macros |\n| `toc` | delete the `<!-- toc -->` marker (or the configured `marker`): the outline rail replaces it |\n| `pagetoc`, `open-on-gh` | built in (outline rail; `github` edit links): delete |\n| `alerts` | GitHub alerts (Admonitions) |\n| `tabs` | `{{#tabs}}` / `{{#tab name=\"X\"}}` … `{{#endtab}}` / `{{#endtabs}}` → `<Tabs>` / `<Tab title=\"X\">`. `global=\"…\"` drops: Blume syncs same-titled tabs page-wide |\n| `i18n-helpers` (gettext, `po/*.po`) | i18n (Repo integration) |\n| `cmdrun` (`<!-- cmdrun … -->`), `template`, custom ones (`command = \"cargo run …\"`) | diff `old-md/` against `src/` to see what each one wrote, then freeze that output or move the generator into a prebuild script (report) |\n| `linkcheck` | `blume validate --strict` |\n| other diagram plugins (graphviz, plantuml, svgbob) | render each diagram to an SVG once and commit it, or redraw it in mermaid (report) |\n| anything else | read what it does; report it |\n\n## Navigation: `SUMMARY.md` → folders and `meta.ts`\n\n### How mdBook reads `SUMMARY.md`\n\n- A `# Summary` first line is ignored. **Prefix chapters** are bare `[Title](file.md)` lines before the first list, and can't nest. **Part titles** are `# Title` headings (h1 only) over the numbered chapters that follow: plain headings, not links. **Numbered chapters** are list items (`-`, `*`, or `1.`) that nest by indentation. **Suffix chapters** are bare links after the last list. A **draft** is `[Title]()` (a greyed-out entry, no page). A **separator** is a `---` line; it can also split one part's list, leaving chapters after it under no visible heading.\n- Only listed chapters render. Any other `.md` under `src/` isn't published, but every non-Markdown file there is copied to the site at its own path.\n- A chapter's URL is its path with `.html`; a `README.md` (any case) becomes `index.html`. **The first chapter is also written to `/index.html`.** Its sidebar label and `<title>` are the `SUMMARY.md` text (plus ` - <book title>`); the page shows its own H1.\n\n### Put every chapter in place\n\nKeep each chapter's path, so its route is the old URL minus `.html`, and keep a table of old URL → file → route as you go: it becomes `redirects`.\n\n- **Every `README.md` chapter → `index.md`.** Its route is the folder's, which `x/index.html` already served. Left as is, Blume publishes it at `/x/README`.\n- **A parent chapter beside its children's folder** (`guide.md` + `guide/`): `git mv guide.md guide/index.md`. The route stays `/guide` and the folder row links to it; left in place, `guide.md` is a separate page beside an unlinked \"Guide\" group. The page moved a level deeper: rewrite its relative links, images, and include paths, and every link to `guide.md`.\n- **Children flat beside their parent** (`ch03-00-concepts.md`, `ch03-01-variables.md`): a `(group)/` folder holding the parent as `index.md` with `slug: ch03-00-concepts`, plus the children. The group adds no URL segment, the `slug` keeps the parent's route, and the group row links to it. Rebase relative paths in every moved file.\n- **The first chapter**, when it isn't the root `README.md`: move it to the content root's `index.md` and redirect its own URL to `/` (`{ from: \"/introduction.html\", to: \"/\" }`), since mdBook served it at both.\n- **Ordering prefixes.** Blume drops a leading `\\d+[-_.]` from every segment, keeping versions (`1.2.0`) and dates (`2024-01-05`, `12-05-2022`, `2024-01`) whole (`00-overview.md` → `/overview`). Every old URL is redirected anyway, so let Blume drop them (they keep ordering the sidebar) and point those redirects at the new routes, or pin `slug` on each page to keep the old path. Say which.\n- **Files `SUMMARY.md` doesn't list** were never published: a contributors' `README.md` beside the chapters, notes, Markdown that `{{#include}}` pulls in. Prefix them with `_`, move them out of the content root, or add them to `content.exclude` (it adds to the defaults `\"**/_*\"` and `\"**/.*\"`). Report them.\n- **A chapter whose body is a `<meta http-equiv=\"refresh\" content=\"0; url=…\">`** is a redirect, not a page to keep: add a redirect from its `.html` URL to the target, a `navigation.featured` link in place of its sidebar entry, and delete it (mdBook's `print.html` ran the same tag, so it sent the whole print page away too). Report it.\n- Delete `SUMMARY.md` once folders and `meta.ts` carry its structure: it would publish as a page.\n- **Check that git sees the new files.** A repo `.gitignore` can cover the content root (pages added before the rule stay tracked), and then every `meta.ts` and partial you create is silently left out of `git add`. Run `git check-ignore -v <content root>/meta.ts` after writing the first one; if it prints a rule, fix that rule or report that each new file needs `git add -f`.\n\n### Parts, order, labels, and folding\n\n- **Display:** set `navigation: { sidebar: { display: \"group\" } }`. Parent chapters become collapsible rows, like mdBook's tree, and a group keeps leaf chapters between its subgroups in `meta.ts` order (SKILL.md: `flat` can't interleave them).\n- **Part titles** → a folder per part, with `title: \"<part title>\"` and `display: \"flat\"` in its `meta.ts`: a heading, not a row. When a part's chapters already share a folder (`developers/`), that folder is the part; otherwise use a `(part)/` folder at the content root, which changes no URL. Chapters after a separator inside a part get the folder they're already in (`appendix/` reads \"Appendix\"), or the part's heading.\n- **Order** → a `meta.ts` `pages` array in each folder, every child in `SUMMARY.md` order. The root `meta.ts` orders the parts.\n- **Prefix chapters** stay loose pages at the content root, which Blume lists first, as mdBook did.\n- **What files can't express:** at the top of the sidebar, loose pages always list above folders. Suffix chapters, and leaf chapters between parent chapters in a book with no parts, rise above them. Put each in a `(group)/` folder (it adds a heading), or accept the order. When many chapters are affected, an explicit `navigation.sidebar` keeps `SUMMARY.md`'s order exactly: a route string per leaf chapter, `{ label, root: \"<route>\", items }` per parent chapter, `{ label, display: \"flat\", items }` per part. It replaces the generated tree, so it needs an entry per new page, as `SUMMARY.md` did. Report which you chose.\n- **Labels:** `title` is the body H1, what the page showed. `sidebar.label` is the `SUMMARY.md` text where it differs, what the sidebar and `<title>` showed. A page with no H1 takes the `SUMMARY.md` text as `title`. Give each parent chapter's folder `title: \"<its SUMMARY.md text>\"` in `meta.ts`, or its row shows the humanized folder name. Strip Markdown from `SUMMARY.md` text: backticks render literally in `title`.\n- A parent's `index` page also lists as its group's first row, so the label shows twice. To show it once, as mdBook did, set `sidebar.hidden: true` on the index: it stays in `sitemap.xml` and search because the group row still links to it. If its `title` differs from the folder's `meta.ts` title, set its `sidebar.label` to the folder title, or `BLUME_NAV_INDEX_TITLE_MISMATCH` fails `validate --strict`.\n- **`[output.html.fold]`:** with `enable = true`, sections below `level` started closed (0 closes all), which is `group` mode's default; add `collapsed: false` to the folders mdBook opened. Without folding, mdBook showed every section open: add `collapsed: false` to every folder's `meta.ts`.\n- **Drafts and separators** have no equivalent: drop them, and report the drafts as planned chapters.\n\n## Content\n\n### `.md` or `.mdx`\n\nRename a page to `.mdx` when it ends up with a callout, mermaid, math, tabs, or another component. Those are MDX-only: in `.md` they don't render, and the build stays green (a `:::` callout only warns `BLUME_MD_DIRECTIVE`). Tables, footnotes, strikethrough, task lists, `[#id]` heading ids, `<include>`, `{{variables}}`, and raw HTML work in both (`{#id}` only in `.md`). Fix what fails `.mdx` in each renamed page: HTML comments → `{/* … */}` on one line, unclosed `<br>`/`<img>`, `<https://…>` autolinks, and `{`, `<` in prose (`Vec<T>` outside backticks).\n\n### Includes and the other `{{#…}}` helpers\n\nmdBook expands these as text before parsing, so they work inside fences, inline code, and link URLs, and across a line break (`{{#include` at a line's end, the path on the next, as Prettier wraps it). A path resolves from the including file.\n\n| mdBook | Blume |\n| --- | --- |\n| a fence holding only `{{#include path}}`, the whole file, inside the content root | the whole fence → `<include lang=\"rust\">path</include>` on its own line (the fence's language; its attributes drop) |\n| a fence holding only `{{#include path:anchor}}`, `path:N`, `path:N:M`, `path:N:`, or `path::M`, or any target outside the content root | an excerpt (below), included the same way |\n| a fence mixing includes with hand-written lines | one excerpt that joins `{ \"text\": \"…\" }` parts and selections in order, so the block stays generated |\n| `{{#rustdoc_include path:sel}}` | an excerpt of `path:sel`: the selected lines only. The rest of the file sat behind mdBook's expand toggle, which has no equivalent (report) |\n| `{{#playground path attrs}}` | `<include lang=\"rust\">` of the file (an excerpt when it's outside the root); no Run button (report) |\n| `{{#include x.md}}` on its own line | `<include>/_snippets/x.md</include>` (a leading `/` is the content root): a partial must sit under a `_` folder or name, or it publishes as a page. Images in a partial resolve from the partial's own folder (mdBook resolved them from the including chapter), so fix their paths |\n| a one-line file included inline (`{{#include ../VERSION}}` in a URL or a command) | `{{VERSION}}`, with `variables: { VERSION: readFileSync(new URL(\"../VERSION\", import.meta.url), \"utf8\").trim() }` in `blume.config.ts` (`import { readFileSync } from \"node:fs\"`). Variables are replaced in links, code, and inline code too |\n| `{{#title X}}` | `seo.title: X` (Blume appends the site title, as for every page) |\n| `\\{{#include x}}` (often in a fence that documents mdBook) | drop the backslash, which mdBook removed even inside code. `{{#include x}}` stays literal in code and in `.md`; in `.mdx` prose a `{` fails the build, so put it in inline code |\n\n**Excerpts.** `<include>` fails the build on a target outside the content root (`BLUME_INCLUDE_OUTSIDE_ROOT`) and can't select lines. Pasting each excerpt into its page would freeze code that cargo still compiles and tests, so generate them before every run with the bundled `include-excerpts.mjs`:\n\n1. Copy `<skill>/scripts/include-excerpts.mjs` beside `blume.config.ts`. Its header documents the manifest.\n2. Write `excerpts.json` there: `out` (`<content root>/_excerpts`, relative to the manifest; Blume doesn't publish a `_` folder), and one entry per excerpt, keyed by its file name under `out`. An include's argument works as written once its path is relative to the manifest's folder (`\"counter/abi.rs\": \"../examples/counter/src/lib.rs:contract_abi\"`): the script selects anchors and line ranges exactly as mdBook did. A mixed fence is an array: `[{ \"text\": \"impl Contract for Token {\" }, \"../src/contract.rs:transfer\", { \"text\": \"}\" }]`.\n3. Run `node include-excerpts.mjs`. It checks every entry first and lists **every** missing file, missing anchor, and line range past the end of its file in one pass, then writes nothing. A clean run writes the excerpts and deletes the ones it wrote earlier that the manifest dropped (never a file it didn't write).\n4. Replace each include with `<include lang=\"rust\">/_excerpts/counter/abi.rs</include>`.\n5. Run it first in `package.json` (`\"dev\": \"node include-excerpts.mjs && blume dev\"`, and the same for `build` and a `validate` script), and gitignore `<content root>/_excerpts/`. A host's build command must be `npm run build`, not `blume build`. Rerun it after editing a source file: `blume dev` doesn't watch them.\n\n**Missing anchors.** For each anchor the script reports (the old site showed an empty block, or only a mixed block's hand-written lines), find where the code went (`git log -S \"ANCHOR: <name>\"`, or search the crate for the item), then:\n\n- **When the team owns the source, re-add the `// ANCHOR: <name>` and `// ANCHOR_END: <name>` comments around the code there.** The entry keeps working as written, and the excerpt follows the code as it changes.\n- Otherwise, point the entry at a line range (`\"…/lib.rs:14:23\"`). It keeps selecting those line numbers when the file changes, without an error, so say so in the report.\n- Drop the block, with its lead-in sentence, when the code is gone.\n\nReport each one and what you chose. Code that now fills a formerly empty block may contradict the prose around it: list what you notice.\n\n### Code blocks\n\n| mdBook | Blume |\n| --- | --- |\n| ` ```rust,ignore `, `rust,no_run,noplayground`, `rust,should_panic`, `rust,compile_fail`, `rust,edition2021`, `rust,editable`, `rust,mdbook-runnable`, `rust,hidelines=…` | ` ```rust ` (a comma-glued language doesn't highlight, and warns `BLUME_UNKNOWN_CODE_LANGUAGE`; written with a space, ` ```rust ignore `, the keyword does nothing and warns `BLUME_CODE_FENCE_OPTION`) |\n| ` ```ignore `, or no language: rustdoc tests an unlabeled block as Rust, so books label diagrams and shell lines `ignore` | ` ```text `, or the real language |\n| **hidden lines** in a Rust block: first non-space character `#`, then a space or the end of the line | delete the line, and a blank line it leaves at the top of the block: readers saw it only through the eye toggle, which has no equivalent. `##x` shows as `#x`; `#[derive]` and `#!` lines are code. Lines with a `[output.html.code.hidelines]` prefix (`python = \"~\"`), or a fence's `hidelines=<prefix>`, go the same way |\n| the play button, `editable` | static code (report) |\n| `mdbook test` | nothing compiles Blume's code blocks. Excerpts stay tested by cargo; hand-written blocks that `mdbook test` compiled aren't any more (report; drop the CI step and whatever it built only for that) |\n\n### Admonitions\n\n**mdbook-admonish** writes ` ```admonish <type> ` with options (`title=\"…\"`, `collapsible=true`, `id`, `class`), the older `admonish <type> \"Title\"`, or `~~~`/four-backtick outer fences around inner code. Each becomes a callout in `.mdx`, with a longer `::::` outer fence around a nested one:\n\n| admonish type (aliases) | Blume |\n| --- | --- |\n| `note`, `example` | `:::note` |\n| `info` (`todo`), `abstract` (`summary`, `tldr`), `question` (`help`, `faq`) | `:::info` |\n| `tip` (`hint`, `important`) | `:::tip` |\n| `success` (`check`, `done`) | `:::success` |\n| `warning` (`caution`, `attention`) | `:::warning` |\n| `danger` (`error`), `failure` (`fail`, `missing`), `bug` | `:::danger` |\n| `quote` (`cite`) | a blockquote |\n| **any other word** (`admonish warn`) | `:::note`: admonish rendered it as a note titled \"Note\". Don't keep the word: Blume reads `:::warn` as a warning. Report it, with the type the author probably meant |\n\n- `title=\"T\"` → `:::type[T]`; `title=\"\"` → no title. An untitled block showed the word it was written with as its title bar (`Example`, `Hint`, `TL;DR`), and an untitled Blume callout shows none: where that word isn't the Blume type's name, keep it as the title (`:::note[Example]`).\n- `collapsible=true` → `<Expandable title=\"T\">` (the type's name when untitled); its color drops.\n- Custom directives (`[[preprocessor.admonish.custom]]`) → the closest callout, with the label as title.\n- Each block had an anchor (`#admonition-<title>`, `-1` for repeats). Links to one fail `blume validate --strict`: point them at the nearest heading or drop the fragment.\n\n**GitHub alerts** (`> [!NOTE]`, built into 0.5 and the `alerts` plugin) render as callouts in `.mdx` as written: `NOTE` and `IMPORTANT` as a note, `TIP` a tip, `WARNING` a warning, `CAUTION` a danger callout. In `.md` they stay a blockquote with a literal `[!NOTE]` and warn `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. A 0.4 book without the plugin showed them as plain quotes: rendering them changes the page (report).\n\n### Other syntax\n\n| mdBook | Blume |\n| --- | --- |\n| `## Heading {#id}` | the same in `.md`; `[#id]` in `.mdx` |\n| `## Heading { #id .class }` | `{#id}` or `[#id]`; classes drop. Left as is, the spaced form stays in the heading text and makes a garbage id; `BLUME_MD_CURLY_ANCHOR` flags it only when no class follows the id (in `.mdx` it fails the build) |\n| footnotes, tables, strikethrough, task lists | unchanged |\n| definition lists (`term` then `: definition`, on by default in 0.5) | a list or table, or raw `<dl>`; Blume renders them as one paragraph |\n| MathJax (`mathjax-support`): `\\\\( … \\\\)`, `\\\\[ … \\\\]` | `$$…$$` inside the sentence, or on lines of its own for a block, in `.mdx`. Undo the Markdown escapes mdBook needed inside the math (`\\\\\\\\` for a LaTeX `\\\\`, any `\\_` or `\\*`): `$$` math is read verbatim |\n| `<i class=\"fa fa-x\"></i>` | `<Icon icon=\"<lucide>\" />` in `.mdx`, or delete (the icon map in `references/mintlify.md`) |\n| `class=\"left\"`/`\"right\"` floats; `class=\"hidden\"` | drop the class; delete hidden elements (report) |\n\n### Links and images\n\n- mdBook only rewrote `x.md` links to `x.html`. Blume resolves a relative `.md` link to the page that file publishes: keep them, and fix the ones into or out of every moved file.\n- Rewrite `.html` links (`installation.html#cargo`) to their `.md` files: Blume serves no `.html` URLs, so they'd depend on the redirects. `blume validate` reports each one left (`BLUME_BROKEN_ASSET`, with the page's route).\n- **A link to `README.md` was already dead:** mdBook wrote `README.html`, a page it never creates. Point it at the renamed `index.md`.\n- Root-relative links (`/guide/x.md`) land on the page that file publishes, read from the content root, like relative ones.\n- Images relative to the page work as written. So does a linked download (a PDF, an archive) that mdBook copied from `src/`: Blume publishes a file a relative link names with the page, at a new URL. Move it to `public/` at the same path only if its old URL is linked from elsewhere.\n\n## URLs and redirects\n\n| Source file | mdBook URL | Blume route |\n| --- | --- | --- |\n| `src/guide/setup.md` | `/guide/setup.html` | `/guide/setup` |\n| `src/guide/README.md` | `/guide/index.html` | `/guide` |\n| the first chapter, `src/intro.md` | `/intro.html` and `/index.html` | `/` once moved to `index.md` |\n| `src/01-basics.md` | `/01-basics.html` | `/basics`, or `slug: 01-basics` |\n\n- **One exact redirect per chapter that isn't an index**, `{ from: \"/guide/setup.html\", to: \"/guide/setup\" }`, built from `old-urls.txt` and your table: a pattern can't strip `.html`. Skip `/index.html` and `/x/index.html`: static hosts serve Blume's `x/index.html` there already.\n- **`[output.html.redirect]`**: each key is a path from the site root, and its value is a URL relative to the key's folder (`\"../infra/x.html\"`), root-relative, or external. Resolve the value against the key's folder, then map it like a chapter (`.html` → route, `index.html` → its folder); external URLs stay. Check each target against the old build: one that already led to a 404 → the page that replaced it, or drop it, and report.\n- **Fragment redirects** (`\"/page.html#old\" = \"other.html#new\"`, 0.4.52 and later): servers never see fragments. A heading renamed on the same page → pin the old id on it (`[#old]`); moved to another page → no server equivalent (report). A deleted page's own entry is its redirect.\n- **No equivalent, no redirect:** `print.html` (the whole book on one page, `noindex`), `toc.html` (the sidebar frame, `noindex`), and `404.html` (Blume writes its own). Mention `export: { pdf: true }` for printing.\n- **Hosts:** a static build writes each redirect as a meta-refresh page at `dist/<from>/index.html`, plus `_redirects` and `vercel.json`. On Netlify use `netlify({ output: \"static\" })` for real 301s (SKILL.md). GitHub Pages reads neither file and serves the meta-refresh page; how it answers `/x.html` there is unverified, so check a few old URLs after deploying.\n- **Heading anchors:** mdBook 0.5 ids match Blume's for most headings; 0.4 lowercases ASCII only (`## Über` → `#Über`), and both count the H1, so `# Install` then `## Install` gives `#install-1`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old old-book` (add `--map dist/blume-redirects.json` when routes changed, built with no host adapter; `--write` to apply), and rebuild until it reports 0.\n- **Check every old URL** after `blume build`. With no host adapter (static), from the Blume project:\n\n  ```bash\n  node -e '\n  const fs = require(\"node:fs\");\n  const moved = new Map(JSON.parse(fs.readFileSync(\"dist/blume-redirects.json\", \"utf8\")).map((r) => [r.from, r.to]));\n  const built = (to) => /^https?:/.test(to) || fs.existsSync(`dist${to.replace(/#.*/, \"\").replace(/\\/$/, \"\")}/index.html`);\n  const misses = fs.readFileSync(\"old-urls.txt\", \"utf8\").split(\"\\n\").filter(Boolean).filter((url) => {\n    const to = url.endsWith(\"/index.html\") ? url.slice(0, -\"index.html\".length) : moved.get(url);\n    return !(to && built(to));\n  });\n  console.log(misses.length ? misses.join(\"\\n\") : \"every old URL resolves\");'\n  ```\n\n  Then run `blume audit --only redirects`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts`: `blume` plus the `dev`/`build`/`preview`/`validate` scripts, with the excerpt step first. Commit the lockfile (SKILL.md step 6).\n- **`.gitignore`:** replace `book/` with `dist/`, `.blume/`, `node_modules/`, `.env.local`, and `<content root>/_excerpts/`.\n- **CI:** replace `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, and the plugin installs with Node 22, an install, `npm run build`, and `npm run validate`. A deploy workflow in another repo needs the same change: list it as a follow-up. For GitHub Pages, follow https://useblume.dev/guides/markdown-docs-github-pages (Actions as the Pages source), with `npm run validate` and `npm run build` in place of its `blume` steps so the excerpts exist, run from the Blume project's folder, and that folder's `dist` uploaded. A deploy that pushes a branch must publish `.nojekyll`, or Jekyll drops Blume's `_astro/` folder; `peaceiris/actions-gh-pages` adds it by default.\n- **`cargo doc` output published beside the book** (`/api/…`): keep building it in CI and copy `target/doc` into the build output, or link to docs.rs. Blume doesn't generate it.\n- **Several builds of the book** (a stable tag and `main`, `MDBOOK_BOOK__LANGUAGE` per language): migrate the current one; older versions → `versions`, or stay on the old host (report). Translations in `po/*.po` (mdbook-i18n-helpers) apply at build time: render each language's Markdown with `MDBOOK_BOOK__LANGUAGE=<xx>` and the Markdown renderer into `<xx>/`, then set `i18n`. Report it.\n- **Prettier** on the book: extend its `*.md` overrides to `*.mdx`, and keep `{/* */}` comments on one line. Prettier 3 skips what `.gitignore` ignores, so a `.gitignore` that covers the content root also makes a `prettier --check` CI step check nothing.\n- **A hand-written `llms.txt` in `src/`:** Blume generates `/llms.txt`, so delete the file. Its intro goes to `agents.llmsTxt.details`. A per-page summary becomes that page's `description` only where it reads well as the page's lede, since Blume shows `description` under the title; report the rest, and the sections that list things other than pages.\n- **Delete** `book.toml`, `SUMMARY.md`, `theme/`, the `additional-css`/`additional-js` files once ported, `mdbook-admonish.css`, `mermaid.min.js`, `mermaid-init.js`, and the `book/` folder. Grep the repo for `mdbook`, `book/`, and the old `.html` URLs (READMEs, CONTRIBUTING, examples, bots).\n\n## Dropped — report these\n\nSection numbers, separators, and draft chapters; suffix chapters and leaf chapters that moved above folders; the print page; the theme switcher and themes beyond one light and one dark; search options; the play button, the editor, and runnable or editable examples; hidden lines and their toggle; `mdbook test` coverage of hand-written blocks; `rustdoc_include` context; admonish title bars, colors, icons, custom directives, and anchors; fragment redirects across pages; the old site's empty blocks from missing anchors, and what you did with each; definition-list styling; Font Awesome icons with no Lucide match; `class=\"hidden\"` content; plugin output you froze (cmdrun, templates, custom preprocessors); a hand-written `llms.txt`; whole-book PDF or EPUB output; and every plugin you removed without an equivalent.\n",".agents/skills/blume-migrate/references/mintlify.md":"# Mintlify → Blume\n\nThe deepest migration. Mintlify declares its **entire navigation in `docs.json`**; Blume derives navigation from the filesystem. The bulk of the work is reshaping content into folders + tabs and translating config, callouts, and icons.\n\n## Detect\n\n- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.\n- A `mintlify` dependency / `mintlify dev` script.\n- Content is `.mdx` under the repo root (Mintlify has no `content.root` — pages live at the top level), with a `/snippets` folder and image dirs like `/images`.\n\nRead `docs.json` first; it drives everything.\n\n## Config: `docs.json`/`mint.json` → `blume.config.ts`\n\nResolve `$ref` includes first (Mintlify splits config across files). Map only what's set:\n\n| Mintlify | Blume | Notes |\n| --- | --- | --- |\n| `name` / `title` | `title` |  |\n| `description` | `description` |  |\n| `logo` (string or `{ light, dark, href }`) | `logo` (string, or `{ image: { light, dark, alt }, text, href }`) | Mintlify's `light`/`dark` nest under Blume's `image`; ensure files land in `public/`. **If the SVG is local and monochrome** (a solid black or white mark), rewrite its `fill`/`stroke` to `currentColor` and collapse `{ light, dark }` to the string shorthand `logo: \"/logo.svg\"` — one file that inherits the theme text color and flips with light/dark automatically. If the logo is a **wordmark** (brand name baked in), set `text: \"\"` so it doesn't render twice beside `title` |\n| `favicon` (string **or `{ light, dark }`**) | **drop the field** — copy the file(s) into `public/` under conventional names (`favicon.svg`/`icon.png`) | Blume auto-detects by filename; there is **no** favicon config field. A `{ light, dark }` source maps to a filename pair: light file → `public/icon.png`, dark file → its `-dark` sibling `public/icon-dark.png` (same directory and extension — convert if the formats differ) |\n| `colors.primary` + `colors.light` | `theme.accent` (`{ light, dark }`) | accent is now **per-mode**: `accent: { light: <colors.primary>, dark: <colors.light> }`. If Mintlify sets only `primary`, collapse to the string shorthand `accent: \"<colors.primary>\"` (applies to both modes) |\n| `colors.dark` | `theme.action` |  |\n| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) |  |\n| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |\n| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |\n| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |\n| `background.decoration` | **drop** | no Blume equivalent |\n| `fonts.family` / `fonts.{heading,body}.family` | `theme.fonts.{display,body}` | a curated slug (kebab-case, e.g. `space-grotesk`) maps directly; any other Google family maps to the object form `{ name: \"Family Name\" }`; a self-hosted font (`fonts.*.src` URLs) maps to `{ name, variants: [{ src, weight }] }` after downloading the files into the project |\n| `banner` | `banner` (`{ content, dismissible, id, link: { href, text } }`) | **only** those keys — drop `banner.color`/`banner.type` |\n| `styling.latex: true` | **drop the field** — block math `$$…$$` renders in `.mdx` with no config | there is **no** `markdown.math` field; Blume writes inline math with two dollar signs too — convert each inline `$…$` to `$$…$$` inside the sentence, checking every pair is math and not currency or a shell variable |\n| `styling.codeblocks.theme` | `markdown.code.theme` (`{ light, dark }`) |  |\n| `search.prompt` | **drop** | no equivalent |\n| `seo.metatags` | `seo.metatags`, minus the tags Blume writes itself | keep verification tokens, `theme-color`, and the like as written; Blume refuses `description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, and `article:*` dates, so move those to their settings (`og:image` → `seo.og` or a page's `seo.image`, `twitter:site` → `seo.x.handle`) and drop the rest. `seo.paths` (folder tags) has no equivalent: set per-page `seo` frontmatter |\n| `seo.indexing: \"all\"` | `search.indexing.includeHiddenPages: true` |  |\n| `variables` (`{{name}}`) | `variables` | same syntax and names (letters, digits, `_`, `-`); pages keep their `{{name}}` references unchanged |\n| `integrations.posthog` (`{ apiKey, apiHost, sessionRecording }`) | `posthog({ key, host })` in the `analytics` list | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`); `sessionRecording: false` → `disable_session_recording: true`; every adapter is imported from `blume/analytics` |\n| `integrations.ga4` (`{ measurementId }`) | `googleAnalytics({ id })` |  |\n| `integrations.gtm` (`{ tagId }`) | `googleTagManager({ id })` |  |\n| `integrations.plausible` (`{ domain, server }`) | `plausible({ domain, host })` | `server` is a bare hostname; `host` is an origin — prepend `https://` |\n| `integrations.fathom` (`{ siteId }`) | `fathom({ site })` |  |\n| `integrations.pirsch` (`{ id }`) | `pirsch({ code })` |  |\n| `integrations.mixpanel` (`{ projectToken, region }`) | `mixpanel({ token, region })` |  |\n| `integrations.amplitude` (`{ apiKey }`) | `amplitude({ key })` |  |\n| `integrations.segment` (`{ key, cdnUrl }`) | `segment({ key, cdn })` |  |\n| `integrations.hightouch` (`{ writeKey, apiHost }`) | `hightouch({ key, host })` |  |\n| `integrations.heap` (`{ appId }`) | `heap({ id })` |  |\n| `integrations.hotjar` (`{ hjid, hjsv }`) | `hotjar({ id, version })` | both numbers |\n| `integrations.clarity` (`{ projectId }`) | `clarity({ id })` |  |\n| `integrations.logrocket` (`{ apiKey }`) | `logrocket({ id })` |  |\n| `integrations.clearbit` (`{ publicApiKey }`) | `clearbit({ key })` |  |\n| `integrations.adobe` (`{ launchUrl }`) | `adobe({ url })` |  |\n| any other `integrations` script | `script({ src, strategy, attributes })` / `vercel()` in the `analytics` list | one `script()` adapter per provider without a factory |\n| `telemetry` (`{ enabled: false }`) | `feedback: false` | Mintlify's switch also turned off its feedback widgets; Blume sends no telemetry of its own, so only the rating needs turning off |\n| Feedback toggles in the Mintlify dashboard (not in `docs.json`) | `feedback: { comments: true }` for contextual (written) feedback | ask whether the site collected written feedback; Blume sends comments through the `analytics` adapters as `feedback_comment` events. Code-snippet feedback, edit suggestions, and raise-issue have no equivalent: report them |\n| `integrations.cookies` (`{ key, value }`), or a content-folder `.js` file that injects Osano or Ethyca/Fides | `consent`, imported from `blume/consent`: `osano({ customerId, configId })` (both IDs from the `cmp.osano.com/<customerId>/<configId>/osano.js` URL), `ethyca({ privacyCenter })` (the origin that serves `fides.js`), or `native()` for Blume's own banner | Mintlify's `cookies` pair only switched off its own telemetry for readers whose localStorage lacked the value; under `consent`, every `analytics` adapter waits for the reader instead. Delete the injector script. Another consent manager (Transcend, OneTrust, …) has no adapter: report it |\n| A root `skill.md`, or `.mintlify/skills/<name>/SKILL.md` | move each into a `skills/<name>/` folder and set `agents.skills: \"./skills\"`; drop Mintlify-only frontmatter (`groups`) | Blume already generates a skill for the site at `/skill.md` (`agents.skillMd`, on when `deployment.site` is set); a root `skill.md` replaces it only if its `name` matches the site's title slug (`Acme Docs` → `acme-docs`), so set that name |\n| `contextual` (`[\"copy\",\"chatgpt\",\"claude\",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `agents.mcp.enabled` + server output (report as a follow-up) |\n| `footer` (`{ socials, links }`) | `footer: { socials, links }` | `socials` keeps its keys, except `twitter`/`x-twitter` → `x` and `earth-americas` → `website`; a key outside Blume's list drops (report); `links` is one row of `{ label, href }`: every column's `items` flatten into it in order, and each column's `header` drops (report) |\n| `api.mdx` (`{ server, auth: { method, name } }`) | `api: { server, auth: { method, name } }` | the defaults for `api`-frontmatter pages |\n| `api.playground` (`{ display, proxy }`) | `api: { playground }` | `display: \"none\"` → `playground: false`; `\"simple\"` has no site-wide form, so set `playground: simple` in each page's frontmatter; `proxy` (on by default in Mintlify) → `playground: { proxy: true }`, which needs a server-output adapter, or leave it off to send directly |\n| `redirects` (`{ source, destination, permanent }`) | `redirects: [{ from, to, status }]` | `source` → `from`, `destination` → `to` as written, patterns included (`/beta/:slug*`, `/old/article-*`); `permanent: false` → `status: 307`, otherwise `status: 308` |\n| `navigation.languages` | `i18n` | see i18n below |\n\nA minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.\n\n## Icons: FontAwesome → Lucide (required)\n\nMintlify defaults to **FontAwesome**; Blume is **Lucide-only**. Convert every icon reference — frontmatter `icon`, nav-group `icon`, `<Icon>`, `<Card icon>` — to the closest Lucide name. Discard Mintlify's `iconType` (solid/regular/brands) entirely.\n\n**Automate the frontmatter pass — don't hand-edit it.** `scripts/mintlify-codemod.mjs` (in this skill, zero-dependency) remaps every **frontmatter** `icon:` using the table below, drops icons with no Lucide equivalent, and — in the same pass — drops/renames unsupported frontmatter keys (see [Frontmatter](#frontmatter)). It's deterministic and idempotent, and reports what it changed per file plus what it couldn't (unknown icons, OpenAPI-stub flags):\n\n```bash\n# <skill> = this skill's directory (the one containing SKILL.md); dry run first (report only), then apply:\nnode <skill>/scripts/mintlify-codemod.mjs <content-dir>\nnode <skill>/scripts/mintlify-codemod.mjs --write <content-dir>\n```\n\nThe codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon=\"…\">`, `<Card icon=\"…\">`, nav-group icons in `docs.json`) it does not see — convert those by hand using the same table. Common mappings:\n\n| FontAwesome | Lucide |  | FontAwesome | Lucide |\n| --- | --- | --- | --- | --- |\n| `bolt` | `zap` |  | `gear`/`cog` | `settings` |\n| `circle-info`/`info` | `info` |  | `wand-magic-sparkles`/`magic` | `sparkles` |\n| `house` | `house` |  | `magnifying-glass` | `search` |\n| `gauge`/`gauge-high` | `gauge` |  | `puzzle-piece` | `puzzle` |\n| `envelope` | `mail` |  | `file-lines` | `file-text` |\n| `pen-to-square` | `square-pen` |  | `trash-can` | `trash-2` |\n| `xmark`/`times` | `x` |  | `circle-check` | `circle-check` |\n| `triangle-exclamation` | `triangle-alert` |  | `circle-exclamation` | `circle-alert` |\n| `arrow-right-from-bracket` | `log-out` |  | `right-to-bracket` | `log-in` |\n| `location-dot`/`map-marker` | `map-pin` |  | `comments` | `messages-square` |\n| `cube` | `box` |  | `cubes`/`boxes` | `boxes` |\n| `layer-group` | `layers` |  | `diagram-project`/`sitemap` | `workflow`/`network` |\n| `chart-line` | `chart-line` |  | `chart-simple`/`chart-column` | `chart-column` |\n| `flask` | `flask-conical` |  | `robot` | `bot` |\n| `screwdriver-wrench`/`toolbox` | `wrench` |  | `circle-question`/`question` | `circle-help` |\n| `life-ring` | `life-buoy` |  | `shield-halved` | `shield` |\n| `rocket`/`book`/`book-open`/`code`/`terminal`/`key`/`lock`/`user`/`users`/`database`/`server`/`cloud`/`bell`/`calendar`/`star`/`heart`/`tag`/`folder`/`globe`/`link`/`download`/`upload`/`check`/`copy`/`play`/`filter` | _(same name — verify)_ |\n\n**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*`): Lucide still ships a few brand marks, and Blume renders them under the same name — `facebook`, `github`, `gitlab`, `instagram`, `linkedin`, `slack`, `twitter`, `youtube`. Keep those. The rest (`discord`, `x-twitter`, `docker`, `npm`, `python`, `react`…) have **no** Lucide equivalent, and FontAwesome's `apple` is the Apple logo while Lucide's `apple` is the fruit. Where no Lucide counterpart exists, **drop the icon and report it**. An icon that only marks a link to the repository or a social profile usually has a better home: the `github` config (renders the footer's repo link) or `footer.socials`. The build won't catch a wrong name for you: an unknown icon renders nothing, and it's only a warning (`BLUME_UNKNOWN_ICON`) for navigation icons — an icon prop on a component like `<Card>` fails silently — so check every name.\n\n## Navigation: `docs.json` `navigation` → filesystem + tabs\n\nMintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:\n\n- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. `directory` (`card`/`accordion`/`none`, on a group, tab, or the navigation itself) → `meta.ts` `directory` on that folder, or on the content root's `meta.ts` for the whole navigation; it inherits down the tree the same way, and lists the pages on the group's `root`/`index` page. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display, directory })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)\n- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.\n- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.\n- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `\"GET /path\"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).\n- **`tabs`** (top-level `navigation.tabs`, e.g. `App` / `API` / `API reference`) → Blume's **header tabs** `navigation.tabs` (`{ label, path, icon? }`). **Keep them as tabs — do not flatten them into `navigation.sidebar` groups or sections.** Flattening is the easy translation error: it collapses the site's top-level structure into one global sidebar and loses the header tab bar entirely. A Mintlify top-level tab is a header tab; map it to one. Put each tab's pages in **one folder** and point the tab's `path` at it — the tab then scopes the sidebar automatically (under the tab's `path`, the sidebar shows only that folder). **Blume resolves the active tab by URL prefix** (the tab whose `path` is the longest prefix of the current route), so every page in a tab must live under that tab's single `path`. Mintlify tabs freely mix pages from any folder / arbitrary routes; Blume tabs scope by prefix, so a tab that pulls in routes from all over is **not portable as-is**. You have two choices, per tab:\n  - **Move the pages under one prefix** so they share the tab's `path` (a route change → add a `redirects` entry for each moved page). This preserves the exact tab grouping.\n  - **Accept the closest shape** — assign each shared/cross-cutting page to one tab's folder and link to it from the others — when moving routes isn't worth the churn.\n\n  Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.\n\n- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.\n- **`anchors` / `navigation.global.anchors`** (persistent sidebar-top links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old \"drop and report\" for anchors.)\n- **`navbar.links`** (plain header links — Log in, Status, Support) → **`navigation.actions`** (`[{ label, href }]`), rendered in the header left of the icon buttons. A GitHub link → the `github` config (or `navigation.repo` as a URL when the docs repo is private) instead; it renders in the footer, the only place Blume links a repository. Header links hide on phones; a link that must survive there belongs in `featured`.\n- **`navbar.primary`** (the header **CTA button** — \"Get Started\"/\"Sign Up\", `type: \"button\"`) → **`navigation.cta`** (`{ label, href }`, singular: Blume renders exactly one filled button). A `type: \"github\"` primary → the `github` config instead. A route the docs don't serve (the product's own `/signup`) must be an absolute URL, or it raises `BLUME_NAV_MISSING_PAGE`.\n- **`languages`** → `i18n`, not a selector (see below).\n- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.\n\nThis reshaping **changes URLs** — a page moved from `getting-started/quickstart` into the API tab's folder becomes `/api/…`, an `index` promotion drops a segment, etc. Record each old→new path and add a `redirects` entry for it (see below); otherwise every existing link and bookmark 404s.\n\n## Content & component transforms\n\nRewrite each page's MDX:\n\n- **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type=\"x\">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. A callout with an `icon` or `color` (Mintlify's generic `<Callout icon=\"key\" color=\"#FFC107\">`) stays a `<Callout>` instead, since a directive can't set them: Blume's takes `type`, `title`, `icon` (remap it with the table below), and `color` (any CSS color); drop `iconType`.\n- **Accordions — container/item inversion!** Mintlify nests `<Accordion title=\"…\">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title=\"…\">`→`<AccordionItem title=\"…\">` (item).\n- **`<Tabs>` → `<Tabs inline>`.** Mintlify renders tabs **borderless** — a strip on a full-width rule with the content flowing beneath as prose — while Blume's `<Tabs>` defaults to a bordered box. Add `inline` to each `<Tabs>` to preserve Mintlify's appearance; every child `<Tab title=\"…\">` is unchanged. Don't add `param` — Mintlify tabs switch in place and don't deep-link to the URL, so plain `inline` is the faithful mapping.\n- **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tab>`, `<Steps>`/`<Step>`, `<View>` (its `title` becomes an option in the page's view picker, as on Mintlify; remap its `icon` with the table below and drop `iconType`). Keep them as-is.\n- **Code-block titles carry over.** Both read every bare word after the language as the title (` ```ts Acme SDK `), and `wrap` and `expandable` mean the same in both. Mintlify's other fence options don't: `lines` → `lineNumbers` (left as is, it does nothing and warns `BLUME_CODE_FENCE_OPTION`), `highlight={1-2,5}` → `{1-2,5}`, `focus={2}` → a `// [!code focus]` comment on each line, and drop `icon=\"…\"`.\n- **`<RequestExample>`/`<ResponseExample>` pass through.** Top-level ones pin to a column beside the page on wide screens, request above response, as on Mintlify; `dropdown` works the same.\n- **Hand-written endpoint pages pass through.** `api: \"POST /v1/users\"` frontmatter, with `authMethod` (`bearer`/`basic`/`key`/`none`) and `playground` (`interactive`/`simple`/`none`), works as on Mintlify: the page's `<ParamField>`s build its Try it panel and request samples. Keep them as written.\n- **API fields pass through.** `<ParamField>` (`path`/`query`/`header`/`body`, `type`, `required`, `deprecated`, `default`, `placeholder`) and `<ResponseField>` (`name`, `type`, `required`, `deprecated`, `default`, `pre`, `post`) render natively, nested `<Expandable>` fields included. Keep them as written. An older `<RequestField>` isn't supported: rewrite it as a `<ParamField>`.\n- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `githubReleases()` source.\n- **Snippets become includes.** Blume has no `/snippets` import mechanism; its reuse is `<include>`. Move each snippet into the content folder as a `_`-prefixed partial (`_snippets/x.mdx`, which never renders as a page), and replace `import X from \"/snippets/x.mdx\"` + `<X prop=\"v\" />` with `<include prop=\"v\">./_snippets/x.mdx</include>` on its own line. In the partial, rewrite each `{prop}` placeholder to `{{prop}}`: include attributes are props read with the variables syntax. Named string imports (`import { foo } from \"/snippets/vars.mdx\"`) → a site-wide `variables` entry read as `{{foo}}`, or inline the value.\n\n## Frontmatter\n\nMintlify page frontmatter → Blume's strict schema. **`scripts/mintlify-codemod.mjs --write` does the mechanical rows automatically** (rename, drop, icon-remap) and flags the judgment rows (`openapi`/`asyncapi`) for you — see [Icons](#icons-fontawesome--lucide-required) for the invocation. The table is both the mapping reference and a description of exactly what the codemod does:\n\n| Mintlify | Blume | Codemod |\n| --- | --- | --- |\n| `title` / `description` | pass through | — |\n| `sidebarTitle` | `sidebar.label` | renames |\n| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |\n| `tag` | `sidebar.badge` | renames |\n| `canonical` | `seo.canonical` | renames |\n| `og:image` | `seo.image` | renames |\n| `hidden: true` | valid top-level in Blume — **kept as-is**; add `noindex: true` yourself if the page must also leave the search index | left (do by hand) |\n| `openapi`/`asyncapi` | usually an endpoint stub → **delete the page** (Blume generates operation pages); else drop the key and keep it as a normal page | **flags** for review — never auto-deletes a page |\n| `api`, `authMethod`, `playground` | kept as written (a hand-written endpoint page, see above) | left |\n| `related` (a list) | kept as written: root-relative paths, URLs, and `{ Title: link }` entries render as Related pages cards | left; `related: true` has no equivalent (Blume has no automatic related pages), so drop it (report) |\n| `mode` | `mode`, as written | `default`, `wide`, `center`, `custom`, and `frame` carry over; `assistant` has no equivalent → drop (report) |\n| `hideFooterPagination: true` | `pagination: false` | renames (`false` drops, since the links show by default) |\n| `keywords`, `boost` | `search.keywords`, `search.boost` | renames |\n| `searchable: false` | `search.exclude: true` | renames (`searchable: true` drops, since pages are searchable by default) |\n| `public`, `rss`, `groups`, `hideApiMarker`, `iconType` | **drop** (report) | drops |\n\nThe codemod leaves the source key in place and reports a conflict rather than clobbering data when a rename target already exists (e.g. a page already has `sidebar.label`) or the value is too structured to move safely — resolve those by hand. Remove any duplicate H1 in the body — `title` renders the H1. (The codemod only edits frontmatter; it never touches the body.)\n\n## OpenAPI\n\nTop-level `openapi`, `api.openapi`, or a per-group/per-tab `openapi` → `reference: [openapi({ sources: [{ spec, label?, route? }] })]`, with `openapi` imported from `blume/reference` (`spec` alone is the single-source shorthand; several per-tab specs become several sources, or several `openapi()` entries when they need different routes; a spec the source embedded with Scalar becomes a `scalar({ spec })` entry). A Mintlify `{ source, directory }` object: `directory` → the source's `route`. An `asyncapi` field maps the same way to `asyncapi({ … })` in the list. **Delete every per-endpoint stub page** (frontmatter `openapi: \"GET /path\"`, `openapi: \"spec.json webhook name\"`, or a `\"GET /path\"` nav entry) — Blume's native renderer generates one real page per operation and per webhook, and shows callbacks on their operation's page. **Add a `navigation.tabs` entry pointing at the reference `route` yourself** (Mintlify's API tab maps to it); the reference does not create a header tab automatically.\n\n- **Vendor the spec.** Mintlify usually points at a spec **URL**. Copying that straight into `spec:` makes every build fetch it at build time — a single point of failure in CI/offline/behind a proxy, and a failed fetch silently drops the reference (leaving the tab pointing at a route that 404s). Prefer downloading it into the repo (`curl … -o openapi/<name>.json`) and pointing `spec` at that local path. If you keep the URL, report the dependency and consider a `prebuild` refresh-with-fallback.\n- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/list-models`: a camelCase tag or id is split into kebab-case, not just lowercased — see SKILL.md \"OpenAPI\") — this differs from Mintlify's endpoint URLs, so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route and flags the ones you miss.\n- **Overlays carry over.** An `openapi: { source, overlays: [...] }` object → that source's `overlays` (`openapi({ spec, overlays })`, or `overlays` on each entry in `sources`), in the same order; Blume applies Overlay 1.0 and 1.1 the same way. An overlay Mintlify found only through its `extends` field (auto-discovered, not listed in `docs.json`) must be listed explicitly: Blume doesn't scan for overlays.\n- **Code samples carry over.** `api.examples.languages` → `openapi({ codeSamples: [...] })`; Blume generates the same 18 languages and accepts Mintlify's keys and aliases (`bash`, `javascript`, `node`, `csharp`, `dotnet`, `c++`, …) as written. `api.examples.autogenerate: false` → `codeSamples: false`. `x-codeSamples` in the spec needs nothing: Blume renders them ahead of the generated samples.\n- **Keep the \"Introduction\" page.** Mintlify commonly has a written intro/auth page in an \"Introduction\" group beside the \"Endpoints\" (openapi) group in the same tab. Keep it: a normal content page placed under the `openapi()` adapter's `route` (e.g. `<root>/api-reference/introduction.mdx`) merges into the reference tab's sidebar alongside the generated operations. Delete only the per-endpoint stubs, not the conceptual pages.\n\n## Assets\n\nMintlify serves every top-level dir (e.g. `/images`) at the site root. Blume serves `public/` at the site root. **Move root asset dirs into `public/`** (`mv images public/images`) — every `/images/...` reference still resolves, unchanged. Move loose root files (`logo.png`, `favicon.png`) under `public/` too.\n\n## i18n\n\n`navigation.languages` (≥2) → `i18n: { defaultLocale, locales: [{ code, label }] }`. The `default: true` language → `defaultLocale`. Translated content already lives in ISO-code directories, which match Blume's `dir` parser — no file moves. Remove any language selector; language switching is handled by i18n.\n\nPer-language `banner`, `navbar`, and `footer` (on `navigation.languages[]`) merge into the one site-wide config, with each label as a map of language code to that language's text: `banner: { content: { en: \"…\", fr: \"…\" } }` (and `link.text`), `navigation.actions`/`cta` labels, and `footer` link labels. Hrefs stay single — Blume moves an internal link into the reader's language itself. A language without an entry shows the default language's text. Report what doesn't merge: a banner or link set for only some languages, or one whose href differs per language.\n\n## Dropped — report these\n\n- **A group's `boost`** (on a `navigation` group, inherited by its pages) → no inherited boost; set `search.boost` on the pages that need it.\n- **Authentication** (password or SSO, set in the Mintlify dashboard) → Blume has none of its own; point the user at host-level protection (Vercel Deployment Protection, Netlify password protection, Cloudflare Access), per the deployment docs' **Private docs** section. It covers the whole site, so a mix of public and private pages needs two sites.\n- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`.\n",".agents/skills/blume-migrate/references/mkdocs.md":"# MkDocs (Material for MkDocs, Zensical) → Blume\n\nMkDocs is a Python generator: one YAML config (`mkdocs.yml`), Markdown in `docs/`, and Python-Markdown extensions for everything beyond CommonMark. Nearly every MkDocs site uses the **Material for MkDocs** theme, so pages are full of extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets, `{ #id }` attribute lists), and navigation is an explicit `nav` list in config (or alphabetical). The work is translating `mkdocs.yml`, rebuilding `nav` as folders and `meta.ts`, and converting the syntax — most of which becomes `.mdx`-only Blume features, and **MDX rejects several things Python-Markdown accepts**.\n\nScope: checked against MkDocs 1.6.1, Material for MkDocs 9.7.6–9.7.7, pymdown-extensions 11.0.1–12.1, mkdocs-redirects 1.2.3, awesome-pages 2.10.1, awesome-nav 3.3.0, literate-nav 0.6.3, mkdocs-static-i18n 1.3.1, mkdocs-macros 1.5.0, mike 2.2.0, an end-to-end migration of astral-sh/uv (84 pages), and the codemod on pdm-project/pdm (23 pages). **Zensical** and **ProperDocs** projects are the same migration. MkDocs 2.0 (a pre-release with a TOML config and no plugins) was not checked.\n\n## Run the codemod first\n\nFrom the folder that holds `mkdocs.yml` (keep the old Python setup until the end, so `mkdocs build` can still produce the old site for comparison):\n\n```bash\nnode <skill>/scripts/mkdocs-codemod.mjs docs           # dry run: report only\nnode <skill>/scripts/mkdocs-codemod.mjs --write docs   # apply\n```\n\nIt reads `mkdocs.yml` (or `zensical.toml`/`properdocs.yml`, `INHERIT` included) for the enabled extensions, the snippets `base_path`, `extend_pygments_lang`, and `nav` titles, then per page: converts admonitions, details, and tabs (indentation-aware, nested); turns snippets into `<include>` (copying ones from outside the content root into `_snippets/`); rewrites fences, `#!lang` inline code, heading ids, attribute lists, keys, `==mark==`/`^^ins^^`, icon shortcodes, `\\(x\\)` math, and abbreviation lines; moves the body H1 (ATX or setext) into `title` (inline Markdown stripped — Blume renders backticks in `title` literally), a differing `nav` title into `sidebar.label`, and a differing frontmatter `title` into `seo.title`; maps `hide`/`status`/`tags`/`icon`; renames to `.mdx` **only** the pages that now need MDX and fixes their comments, autolinks, and void tags; renames `README.md` to `index.md`; and rewrites links and `<include>`s to every renamed file. It is idempotent.\n\nIt **reports**, without changing, what needs judgment: mkdocstrings blocks, macros/Jinja, template overrides, hooks, every plugin (with a hint), mike and `extra.alternate`, snippet line ranges, `///` blocks, grid cards, code annotations, unknown icons, raw `<img>`s with a relative `src`, pages left with no title, snippet files that publish as pages too, `nav` entries with no file, and every MDX hazard left in a renamed page. Work through that report with the sections below. Nav, config, redirects, and repo integration stay yours.\n\nAfter the first `blume build`, pin the old heading ids (MkDocs and Blume slug headings differ — see URLs). Point `--old` at the old site's built HTML (`mkdocs build -d <dir>` before you delete `mkdocs.yml`) or its live URL, and run it from the Blume project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url>           # dry run\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url> --write\n```\n\nRebuild and rerun until it reports 0 (add `--map dist/blume-redirects.json` when pages moved).\n\n## Detect\n\n- **`mkdocs.yml` / `mkdocs.yaml`** — Material when `theme.name: material`; the plain `mkdocs`/`readthedocs` themes migrate the same way with fewer extensions. Sometimes in a subfolder (`docs/mkdocs.yml`), which `blume migrate` doesn't detect from the repo root: name the source, and run the codemod from that folder.\n- **`zensical.toml`** — Zensical's format: MkDocs' keys in TOML, usually under `[project]` (`[project.theme]`, `nav = [ { \"Title\" = \"page.md\" } ]`); extensions as `[project.markdown_extensions]` keys (`admonition = {}`, `pymdownx.tabbed.alternate_style = true`) or `[project.markdown_extensions.<name>]` tables. With **no extensions configured**, Zensical enables a default set (admonition, details, tabbed, attr_list, md_in_html, keys, mark, caret, arithmatex, emoji, and more, but not snippets); the codemod applies it to a `zensical.toml`. Zensical also builds a plain `mkdocs.yml` and applies the same defaults when it sets no `markdown_extensions`: the codemod can't tell, so list the extensions the pages use in a copy of the config and pass it with `--config`.\n- **`properdocs.yml` / `.yaml`** — ProperDocs, a continuation of MkDocs 1.x.\n- Python deps, not `package.json`: `mkdocs`, `mkdocs-material`, `zensical`, `properdocs` in `requirements*.txt` or a `pyproject.toml` docs group; `mkdocs build`/`gh-deploy` in CI; `.readthedocs.yaml` with an `mkdocs:` key.\n\nResolve the config first: `INHERIT: base.yml` deep-merges this file over another (a repo may have several configs on one base — migrate the public one, report the rest). `!ENV [VAR, default]` reads an environment variable: use the default and report it. `!relative` and `!!python/name:` tags break JS YAML parsers — read the file as text.\n\n**Where the Blume project goes.** `blume build` empties `<project>/dist`, and a Python or Rust repo root often uses `dist/` for wheels or builds. When `mkdocs.yml` sits at the root of a code repo, put `blume.config.ts` and `package.json` **in the docs folder** with `content.root: \".\"` and `content.include` scoped to the page folders (`[\"index.mdx\", \"{guides,concepts,reference}/**/*.{md,mdx}\"]`), and say so. That also keeps a `package.json` out of a non-JS repo root.\n\n**Generated pages.** A `nav` entry or link whose file isn't in the repo is generated at build time (`gen-files`, a hook, a CI step, a `.gitignore`d output — the codemod lists them). Convert today's output once so the site builds, then report that the **generator must change**: until it writes Blume syntax, CI regenerates the old one and Blume renders it as text with a green build. If conversion renames the output (`settings.md` → `settings.mdx`), update `.gitignore`, `.prettierignore`, and the generator's own check mode too.\n\n## Config: `mkdocs.yml` → `blume.config.ts`\n\nMap only what's set:\n\n| MkDocs | Blume |\n| --- | --- |\n| `site_name` / `site_description` | `title` / `description` |\n| `site_url` | Its origin → `deployment.site` (unless the target is Vercel or Netlify — see SKILL.md); **a path in it** (`https://docs.acme.com/uv/`) → `deployment.base: \"/uv\"`, on every host. mike adds its version segment after `site_url`, so it isn't part of the base (Versioning) |\n| `docs_dir` | `content.root` (`docs` is the default) |\n| `use_directory_urls` | no setting — see URLs |\n| `repo_url` on GitHub | `github: { owner, repo }` |\n| `edit_uri` / `edit_uri_template` | **`github.branch`** from the segment after `edit/` or `blob/`. Unset with a GitHub `repo_url` means `edit/master/docs/` → `branch: \"master\"` (Blume defaults to `main`). The path after the branch is `docs_dir`, which Blume adds — **not** `github.dir` (that's only for a project in a repo subfolder). `edit_uri: \"\"`, or Material without the `content.action.edit` feature, shows no edit button; Blume has no separate switch, so accept edit links or leave `github` unset and set `navigation.repo: \"<repo_url>\"` (a footer mark) — say which. Leave `github` unset if edit links would point at gitignored generated files |\n| `repo_url` elsewhere (GitLab…) | no edit links; a header link → `navigation.actions` (report) |\n| `repo_name`, `site_author`, `site_dir`, `dev_addr`, `remote_*`, `watch`, `strict`, `validation` | drop |\n| `copyright` | `footer.copyright`, plain text: strip any HTML tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`) |\n| `exclude_docs` | `content.exclude` — the list adds to the default `\"**/_*\"` and `\"**/.*\"`; `\"!**/_*\"` publishes `_` files |\n| `draft_docs` / `not_in_nav` | `draft: true` / `sidebar.hidden: true` on those pages |\n| `extra_css` | a project-root **`theme.css`**: Material's `--md-*` variables and `.md-*` classes don't exist — port only rules that still mean something, and read colors out of it (Theme) |\n| `extra_javascript` | delete MathJax/KaTeX/Mermaid loaders. Anything else → move into `public/` and `script({ src })` in `analytics` (from `blume/analytics`); a root-relative `src` (`\"/js/x.js\"`) loads under `deployment.base`. Code written against Material's `document$` or `.md-*` DOM won't run — report it |\n| `extra.social` | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube; `twitter`/`x-twitter` → `x`); the site's own repo is covered by `github`; other platforms drop (report) |\n| `extra.analytics` (`provider: google`) | `googleAnalytics({ id: property })`. A `custom_dir` partial `partials/integrations/analytics/<name>.html` is a custom provider: use its adapter (`fathom({ site })` from `data-site`, …) or `script()`. `feedback` → Blume's page feedback is on by default |\n| `extra.consent` | `consent: native()` (from `blume/consent`) |\n| `extra.version` (`provider: mike`) | Versioning |\n| other `extra` keys used as `{{ }}` | `variables` (Manual decisions: macros) |\n| `markdown_extensions` | nothing to configure. `toc.toc_depth: N` → `toc: { maxHeadingLevel: N }` (a `\"2-5\"` range: its upper bound) |\n| `plugins`, `hooks`, `theme.custom_dir` | Plugins; Manual decisions |\n\n### Theme\n\n| Material | Blume |\n| --- | --- |\n| `theme.palette` | one palette with `scheme: slate` → `theme.mode: \"dark\"`; with `default` → `\"light\"`; a list with toggles → system (the default) |\n| `palette.primary` (link color) | `theme.accent`. Presets `red`, `pink`, `purple`, `blue`, `green`, `orange`, `teal` map by name. Other names aren't usable CSS (`deep purple` warns `BLUME_THEME_COLOR_INVALID`, CSS `indigo` isn't Material's): use Material 9.7's link color (`--md-typeset-a-color`) as `{ light, dark }` — indigo `#4051b5`/`#5488e8`, deep purple `#7e56c2`/`#a47bea`, light blue `#02a6f2`, cyan `#00bdd6`, deep orange `#ff6e42`/`#ff764d`, brown `#795649`/`#c1775c`, amber `#d19d00`/`#ffc105`, lime `#8b990a`/`#cbdc38`, light green `#72ad2e`/`#8bc34b`, yellow `#b8a500`/`#ffec3d`; grey, blue grey, black, and white all link in `#4051b5`/`#5e8bde`. `custom` or a custom `scheme:` is defined in `extra_css`: take `--md-typeset-a-color` per scheme (and `--md-default-bg-color` → `theme.background` when it isn't plain white/black) |\n| `palette.accent` | drop |\n| `theme.font.text` / `.code` | `theme.fonts.body` (and `display`) / `mono` as `{ name: \"…\" }`; `font: false` → drop |\n| `theme.logo` (a file in `docs_dir`) | move into `public/`, `logo: \"/logo.svg\"` (SKILL.md on `currentColor` and wordmarks). `theme.icon.logo` → drop and report |\n| `theme.favicon` | move to `public/favicon.<ext>` — no config field |\n| `theme.icon.*`, `theme.language` (English) | drop. A non-English single-language site: the schema accepts a one-locale `i18n` block — check the UI language in `blume dev` |\n\n`theme.features`:\n\n| Feature | Blume |\n| --- | --- |\n| `navigation.tabs` | **`navigation.tabs`**, one per top-level `nav` section, each in one folder (SKILL.md \"Tabs\") |\n| `navigation.sections` | top-level sections stay Blume's default `flat` headers; set **`display: \"group\"` in the `meta.ts` of every second-level section**: Material renders those as collapsible, and a `flat` group lists its pages above its subgroups, reordering a section that interleaves them |\n| neither `sections` nor `tabs` | `navigation.sidebar.display: \"group\"` (Material's collapsible tree) |\n| `navigation.expand` | `collapsed: false` in those folders' `meta.ts` |\n| `navigation.indexes`, plugin `section-index` | a folder's `index` page links its group header. Blume also lists it as the group's first row, so the label shows twice. To show it once, as Material does, set `sidebar.hidden: true` on the index: that drops only the duplicate row, and the page stays in `sitemap.xml`, search, and `llms.txt`. A hidden index whose `title` differs from its folder's `meta.ts` `title` warns `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`: match the two titles, set the index's `sidebar.label` to the folder title to keep its heading, or leave that index's row showing |\n| `content.tabs.link` | same-titled tabs sync within a page, not across pages |\n| `content.action.edit` / `.view` | `github` |\n| `announce.dismiss` | `banner.dismissible`; the text is `main.html`'s `{% block announce %}` → `banner.content` |\n| `content.code.annotate` | no equivalent (the codemod reports annotation markers) |\n| `content.code.copy`/`.select`, `navigation.footer`/`.path`/`.top`/`.instant*`/`.tracking`/`.prune`, `toc.*`, `search.*`, `header.autohide`, `content.tooltips` | drop — built in or no equivalent. Blume's copy button copies only the commands of a `console` or `shellsession` block, without prompts or output, so a custom script that stripped them (`extra_javascript`) can go once those blocks use one of the two |\n\n## Navigation\n\nBlume builds the sidebar from folders (SKILL.md \"Navigation is the file tree\"). Translate `nav` into folders and `meta.ts` (the codemod already set each page's `title`/`sidebar.label`):\n\n- **`- Section: [ … ]`** → a folder; a label that differs from the humanized folder name (\"Getting started\" vs \"Getting Started\") → `meta.ts` `title`. List order → `meta.ts` `pages`, every child. Root order → the content root's `meta.ts`.\n- **A section holding an `index.md`** (with `navigation.indexes`) has that page as its own → nothing to do (see `navigation.indexes` above). Without the feature, Material lists the index as an ordinary row, which is what Blume does anyway.\n- **Nesting that doesn't match folders** is SKILL.md's \"Config-declared nesting\". Decide per section and report:\n  - **One flat folder split into subsections** (`Advanced:` listing `guides/webhooks.md`): a **`(group)/` folder** first, as SKILL.md says — `guides/(advanced)/webhooks.md` keeps `/guides/webhooks` and adds the sidebar group, with no redirects.\n  - **A folder shown under another section** (uv's `pip/` under \"Concepts\"), where a group folder would change the URL segment: **move the files and pin `slug: <old route>` on each moved page**. The sidebar follows the file and the URL follows the slug, with no redirects and no explicit sidebar. Use it when the source kept URLs on purpose.\n  - Otherwise, move the files and add `redirects` (the URLs change), or declare the shape in an explicit `navigation.sidebar` (it replaces the whole generated tree).\n  - Either move puts a page a folder deeper: re-base its relative links and assets, and rewrite every link to it to name the file as it now is (a link to `page.md` resolves to a slugged page only when it names that exact file).\n- **`- Title: https://…`** → `navigation.featured` (top level) or `navigation.actions`; a `nav` entry to a non-page file (`llms.txt`, a PDF) → a featured link.\n- **Pages left out of `nav`** → `sidebar.hidden: true` (note: this also drops them from `sitemap.xml`).\n- **No `nav`**: MkDocs lists each folder's pages (`index` first, then by filename) before its subfolders, labeled \"Getting started\"-style; Blume sorts by title. Add `meta.ts` `pages`/`title` where that differs.\n- **`awesome-pages` `.pages` / `awesome-nav` `.nav.yml`** → a `meta.ts` per folder: `title` → `title`; `nav` → `pages` (drop awesome-pages' `...` and awesome-nav's glob entries like `\"*\"`; an awesome-pages `nav` without `...` hides everything unlisted → `sidebar.hidden: true`; `- Title: page.md` → `sidebar.label`); `hide: true` → hide its pages; sorting, `append_unmatched`, and `collapse*` options drop (report). Delete the files.\n- **`literate-nav`** (`SUMMARY.md`): its nested link list is the nav — convert it like `nav` and delete it (it would publish as a page).\n\n## URLs and redirects\n\nSave the old URL list first — every `<loc>` in the old `site/sitemap.xml` (or the live one) plus every `redirect_maps` key — and check it at the end.\n\n| Source file | MkDocs URL (`use_directory_urls: true`) | (`false`) | Blume route |\n| --- | --- | --- | --- |\n| `docs/a/b.md` | `/a/b/` | `/a/b.html` | `/a/b` |\n| `docs/a/index.md` | `/a/` | `/a/index.html` | `/a` |\n| `docs/a/README.md` | `/a/` | `/a/index.html` | `/a/README` until renamed `index.md` (the codemod does) |\n| `docs/01-setup.md` | `/01-setup/` | `/01-setup.html` | **`/setup`** — Blume strips numeric prefixes; redirect, or pin `slug: 01-setup` |\n\n- **Trailing slashes**: a static build writes `<route>/index.html`, so slashed URLs keep working on static hosts; `vercel()` 308s them; Cloudflare Workers needs `html_handling: \"drop-trailing-slash\"`. `blume dev` and `blume preview` redirect a slashed URL to the slashless page, so old URLs can be checked with or without the slash. **Write targets in any client-side script** (anchor-redirect maps) slashless: Blume's URLs have none.\n- **`use_directory_urls: false`**: one exact redirect per non-index page (`{ from: \"/a/b.html\", to: \"/a/b\" }`; a pattern can't strip a suffix). Hosts that read `_redirects`/`vercel.json` answer them; GitHub Pages, which reads neither, is unverified.\n- **`redirects` plugin**: each `redirect_maps` key and value is a Markdown path relative to `docs_dir` → `{ from: <route of key>, to: <route of value> }`, `#anchor` kept; an `http(s)` value stays a URL.\n- **Anchor-level redirects** (a script mapping `/old/#section`): servers never see fragments. Keep the script (`script({ src })`, targets slashless) or drop it; report either way.\n- **Heading ids**: MkDocs' default slugify strips accents, collapses punctuation runs (`A & B` → `a-b`), and numbers duplicates `_1`; Blume keeps accents, writes `a--b`, and numbers `-1`. Run `pin-heading-ids.mjs` (above) — don't compare by hand.\n- **Links**: the codemod rewrites links to renamed files. After you move or rename anything yourself, point links at the real filenames again — a `page.md` link to a file now named `page.mdx` still lands on its page (Blume tries the other extension, `slug` included), but breaks wherever the Markdown is read as files, such as on GitHub. Links inside an included partial resolve from the **including** page, as with MkDocs snippets. A link to a file moved into `public/` becomes root-relative. A link MkDocs couldn't map to a file (`../install/`, written against MkDocs' slashed URLs) lands one level higher in Blume on any page that isn't an `index`, because Blume reads relative links from the page's folder: point it at the `.md` file. `blume validate --strict` catches the rest. The codemod percent-encodes angle-bracket destinations (`[x](<https://…(…)>)` → `…%28…%29`), which only tidies: Blume reads both forms, and rebases a partial's `<…>` image path onto the including page.\n- **llms.txt plugins**: Blume writes `/llms.txt` and `/llms-full.txt`. The plugin's `/<route>/index.md` copies become `/<route>.md`: add one exact redirect each (a pattern can't map them; `blume audit --only redirects` accepts `.md` targets), and reword any URL advice carried into `agents.llmsTxt.details`.\n\n## Which pages become `.mdx`\n\nDirectives, components, math, and Mermaid work only in `.mdx`, and so does an `<include>` indented inside a list item (a `.md` page reads it as indented code and ships the bare tag); the codemod renames exactly the pages that use them (or include a partial that does) and keeps the rest `.md`. MDX **fails the build** on Python-Markdown-valid source, and the codemod reports what's left in renamed pages: `<!-- -->` comments (→ `{/* … */}` on **one line**), `<https://…>` autolinks, unclosed `<br>`/`<img>`, `<placeholder>` text in prose (`uv run <command>` → backticks), and any `{…}` in prose — attribute lists, critic markup, Jinja, `{name}`. Most fail to compile, and `blume check` (and `validate`, `dev`, `build`) names each at its line as `BLUME_MDX_SYNTAX`; it also names an attribute list (`BLUME_MDX_ATTRIBUTE_LIST`: move the attributes onto a JSX element, or drop them) and an unclosed void tag (`BLUME_MDX_UNCLOSED_ELEMENT`: close it, `<br />`). `{ width=\"300\" }`, `{target=\"_blank\"}`, and `{name}` compile and then fail the render, which `blume build` reports as `BLUME_MDX_UNDEFINED_NAME` at the expression's line. In a `.md` page none of these fail and no extension syntax renders: it ships as text (`--8<--` becomes \"–8<–\", `*[HTML]: …` shows verbatim), and an attribute list warns `BLUME_MD_ATTRIBUTE_LIST`. Smart punctuation turns prose `--flag` outside backticks into an en dash.\n\n## Content\n\nWhat the codemod converts, for checking its output, and what it leaves for you:\n\n| MkDocs | Blume |\n| --- | --- |\n| `!!! type \"Title\"` (body indented 4) | `:::type[Title]` … `:::` (longer `::::` outside nested ones). Material styles 12 types; others render like `note`. Mapped: `abstract`/`question` → `info`, `failure`/`bug` → `danger`, `example`/`important`/`todo` → `note`, `caution`/`attention` → `warning`, `hint` → `tip`, `quote` → a blockquote. Untitled callouts lose Material's \"Note\" title bar; a bracket title keeps its inline Markdown (an unbalanced `]` flattens it) |\n| `??? type \"Title\"` / `???+` | `<Expandable title=\"Title\">` / `defaultOpen`; the type's color drops, and the title keeps its inline Markdown (the codemod strips raw HTML, which would show as text) |\n| `=== \"Tab\"` | every tab one code block → `<CodeGroup>` with the label as `title=\"…\"`; otherwise `<Tabs>`/`<Tab title>` (`===+` → `defaultTabIndex`, `===!` → a new group) |\n| `hl_lines=\"2 4-5\"`, `linenums=\"1\"`, `{ .py title=\"x\" }`, options without a language | `{2,4-5}`, `lineNumbers` (a start other than 1 drops), `py title=\"x\"`, `text {…}`. One left over warns `BLUME_CODE_FENCE_OPTION` (with the Blume spelling) or `BLUME_UNKNOWN_CODE_LANGUAGE` |\n| `pycon`, `pwsh-session`, `extend_pygments_lang` names | `python`, `powershell`, the mapped lexer (any other name Shiki lacks warns `BLUME_UNKNOWN_CODE_LANGUAGE`: map it or write `text`) |\n| `` `#!python x()` `` | `` `x(){:python}` `` |\n| `{ #id }`, `{: #id }`, `{#id}` on headings, also beside classes (`{ #id .wide }`) | `[#id]`, classes dropped (a self-link like `### [x](#x) {: #x }` drops). One left over warns `BLUME_MD_CURLY_ANCHOR` in `.md` (only `{#id}` pins there), and `BLUME_MDX_CURLY_ANCHOR` in `.mdx` (`BLUME_MDX_ATTRIBUTE_LIST` beside classes): write `[#id]` |\n| other attribute lists | dropped and reported (one left over warns `BLUME_MD_ATTRIBUTE_LIST` in `.md`): buttons → links or `<Card>`s (a homepage CTA → `navigation.cta`); image sizing → `<img … />` or `<Frame>`; `{target=\"_blank\"}` → `markdown.externalLinks: true` |\n| `++ctrl+c++`, `==x==`, `^^x^^` | `<kbd>Ctrl</kbd>+<kbd>C</kbd>`, `<mark>`, `<ins>` |\n| `:material-check:` (mapped icons) | `<Icon icon=\"check\" />`; `:octicons-link-external-16:` drops (see `externalLinks`) |\n| `\\(x\\)`, `\\[`…`\\]` | `$$x$$`, `$$` blocks |\n| `*[HTML]: …`, `[TOC]` | removed |\n| `<div markdown>` | attribute dropped (Markdown inside renders in `.mdx`) |\n| Body `# H1` | frontmatter `title` |\n| **By hand:** single-`$` math | `$$…$$` inside the sentence, where it is math |\n| definition lists | `<dl>` or a list |\n| `///` blocks (pymdownx.blocks) | the same targets as `!!!`/`???`/`===`; `/// caption` → `<Frame caption>` |\n| grid cards (`<div class=\"grid cards\" markdown>`) | `<CardGroup>`/`<Card>` |\n| code annotations (`# (1)!` + a list) | prose after the block |\n| `![](x#only-light)` / `#only-dark` | a `theme.css` rule: `:root:not([data-theme=\"dark\"]) img[src$=\"#only-dark\"], :root[data-theme=\"dark\"] img[src$=\"#only-light\"] { display: none; }` (checked in a browser) |\n| emoji `:smile:`, magiclink `#123`/`@user` | paste the emoji; write the links |\n| unmapped icons | a Lucide name in `<Icon>` (verify at lucide.dev), or drop; brand icons (Python, Linux, Docker) have none |\n\n### Snippets and includes\n\n- Snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), never the page; the codemod rewrites them to `/`-rooted `<include>`s.\n- Includes must live in the content root. The codemod copies outside files (`CONTRIBUTING.md`, `examples/`) into `_snippets/`; if the source keeps changing, ship a small `predev`/`prebuild` script that re-copies it, and gitignore the copy. Drop a copied file's H1 (the codemod reports it) — the page's title already renders one.\n- Line ranges (`file.py:3:10`) have no `<include>` equivalent: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (`lines`, run before every build so it keeps tracking the source). Sections (`file.md:name`, marked `--8<-- [start:name]`) aren't read by that script: split the section into its own file and report it.\n- `auto_append` link-definition files: `<include>` the file at the end of each page that uses them. An included file's definitions resolve references in the including page, as if written inline (and the page's resolve in the partial).\n- A partial is parsed with the page that includes it: its directives render only in an `.mdx` page. An HTML comment in a `.md` partial stays hidden in an `.mdx` page too.\n- A snippet file that isn't under a `_` or `.` folder or name is a page too, and publishes on its own as it did in MkDocs; the codemod reports each one. Move it under `_snippets/` unless it should stay a page.\n- `include-markdown` (`{% include \"./x.md\" %}`): `./` paths are page-relative like Blume's, bare ones `docs_dir`-relative (→ `/x.md`); `start`/`end` excerpts → their own file.\n\n## Frontmatter\n\nThe codemod maps `hide: [toc]` → `mode: wide`, `hide: [navigation]` → `mode: center`, `hide: [footer]` → `pagination: false`, `status: new` → `sidebar.badge: New` (`deprecated` → `deprecated: true`), `tags` → `search.tags` (a hosted-search facet; tag pages drop), `icon: material/…` → a Lucide name, and drops `subtitle`, `template`, `social`, `comments`, `render_macros`. `search.exclude`/`boost` keep their meaning. It reports every other key Blume's strict schema rejects (`BLUME_FRONTMATTER_INVALID`) — map or remove them; `.meta.yml` defaults (the `meta` plugin) apply to every page in their folder.\n\n## Plugins — map, don't drop\n\n| Plugin | Blume |\n| --- | --- |\n| `search` | built in — delete |\n| `redirects` | `redirects` (URLs) |\n| `git-revision-date(-localized)` | `lastModified: \"git\"` (it shows a visible \"Last updated\" line) |\n| `git-committers`, `git-authors`, `minify`, `privacy`, `offline`, `typeset`, `info` | drop |\n| `social`, `glightbox`, `optimize` | built in: Open Graph images, image zoom, image optimization |\n| `group` | unwrap and map its plugins |\n| `exclude` | `content.exclude` |\n| `meta`, `tags` | Frontmatter |\n| `blog`, `rss` | Blog |\n| `awesome-pages`, `awesome-nav`, `literate-nav`, `section-index` | Navigation |\n| `include-markdown` | Snippets |\n| `llmstxt` | built in: `markdown_description` → `agents.llmsTxt.details` (reword URL advice); `sections` drop |\n| `macros`, `mkdocstrings`, `mkdocs-click`, `gen-files`, `mkdocs-jupyter` | Manual decisions |\n| `i18n`/`static-i18n`, `mike` | i18n; Versioning |\n| anything else | read what it does; report it |\n\n## Manual decisions — stop and report\n\n- **mkdocstrings** (`::: package.module`): no Python API generator in Blume. Keep that reference published elsewhere and link to it, use `openapi()` for an HTTP API, or hand-write the key pages; redirect the URLs you drop. The same for `mkdocs-click` CLI references and `gen-files` output.\n- **macros**: plain values (`extra`, `include_yaml`, `{% set %}`) → `variables`, same `{{ name }}` syntax — names take letters, digits, `_`, `-` only, and `{{ config.x }}`/`{{ page.x }}` must be inlined. `{% include 'x.md' %}` (from `include_dir`, default `docs_dir`) → `<include>/x.md</include>`. Macro calls and control flow → expand into plain content (a one-off render script is fine); strip `{% raw %}` wrappers.\n- **Template overrides** (`custom_dir`) and **hooks**: read each. Announcement → `banner`; `<head>` meta tags → `seo.metatags` (minus the tags Blume owns); analytics partials → adapters; `<title>`/JSON-LD tweaks → Blume's own; the rest → layout slots (`defineComponents`) or `blume eject`.\n- **Notebooks** (`mkdocs-jupyter`): export to Markdown or link out.\n\n## i18n\n\n`static-i18n` `languages` → `i18n: { defaultLocale, locales: [{ code, label }] }` (both serve the default at the root, others under `/<locale>/`). `docs_structure: suffix` (`page.fr.md`, the default) → `parser: \"dot\"`; `folder` → the default `dir` parser with the **default locale's folder moved up to the content root**. `nav_translations` → per-locale `meta.ts` titles; `fallback_to_default` matches `fallbackLocale`. Pin translated headings with `[#id]`.\n\n## Versioning (`mike`)\n\nmike publishes built HTML per version (`/<version>/…`, aliases like `/latest/`); old versions' sources live only in git tags. Migrate the current version unless asked: serve it at the root and redirect `/<alias>/:path*` and `/<version>/:path*` to `/:path*`. Older versions stay on the old host (a `navigation.featured` link) or are converted from their tags into Blume's `versions.archived` (ids start with a letter). Report which.\n\n## Blog (Material `blog` plugin)\n\nPosts in `blog/posts/` → `type: blog` pages. `date: { created, updated }` → `date` + `lastModified`; `authors: [id]` → inline objects from `.authors.yml`; `categories` → `search.tags` or drop. Keep the old dated URL with a full-route `slug: blog/2024/01/31/my-post`, or redirect. Delete `<!-- more -->` in `.mdx`. Blume has no blog index, archive, category, or pagination pages: write `blog/index.mdx` (`docs/advanced/blog.mdx`), report the rest. RSS moves to `/blog/rss.xml`; with the `rss` plugin (mkdocs-rss-plugin), redirect its `/feed_rss_created.xml` and `/feed_rss_updated.xml` (and `/feed_json_*.json`) there.\n\n## Repo integration and teardown\n\n- **Assets**: everything non-Markdown in `docs_dir` that's served by URL — logo, favicon, downloads, `CNAME`, verification files — moves to `public/`. Images and other files that pages link relatively can stay: Blume publishes each with the page, though at a new URL, so a download whose old URL is linked from elsewhere moves to `public/` too.\n- **Renamed files**: after the codemod, **grep the whole repo for the old page paths** — release tooling that bumps versions inside pages (`[tool.rooster] version_files`, `bumpversion`), Renovate/Dependabot file globs, CODEOWNERS, `.prettierignore`, CI path filters, and scripts.\n- **Prettier**: `.prettierrc` and `.editorconfig` overrides keyed on `*.md` stop applying to the new `.mdx` files — extend them to `*.mdx` before reformatting. Prettier turns a multi-line `{/* … */}` into `{/_ … _/}` (keep MDX comments on one line) and rewrites a URL containing parentheses as a `<…>` destination (fine in a page; percent-encode it in a partial, see Links). If the repo uses Ultracite/oxfmt, see `references/monorepo.md` §6.\n- **CI**: replace `mkdocs build`/`gh-deploy` with Node 22, an install, `blume build`, and `blume validate --strict` (GitHub Pages: the \"Deploy Markdown docs to GitHub Pages\" guide); a `.readthedocs.yaml` build is a hosting change to report. Add `.gitignore` entries for `node_modules/`, `.blume/`, `dist/`, and `.env.local`.\n- **Teardown**: delete `mkdocs.yml` (and `INHERIT`ed bases), `overrides/`, `.pages`/`.nav.yml`/`SUMMARY.md`/`.meta.yml`, hooks, and `extra_css`/`extra_javascript` files once ported. Leave the Python docs dependencies for a follow-up: removing a `pyproject.toml` docs group needs a lock regeneration (`uv lock`), and CI running with `--locked`/`UV_LOCKED=1` fails until it's done — report it.\n\n## Leftover check\n\nRerun until it prints only what you mean to keep (code samples, and `{{name}}` variables you defined):\n\n```bash\ngrep -rnE '^\\s*(!!!|\\?\\?\\?\\+?|===[+!]*) |^\\s*/{3,} ?[a-z]|--8<--|\\{ *[.#:][^}]*\\}|\\{\\{|\\{%|<!--|:(material|octicons|fontawesome|simple)-[a-z0-9-]+:|\\+\\+[a-z]|==[^= ][^=]*==|\\{\\+\\+|\\{--|^\\*\\[|`#![a-z]|hl_lines=|linenums=' docs\n```\n\n## Dropped — report these\n\nTemplate overrides and hooks with no slot equivalent, links inside `copyright`, code annotations, admonition title bars and collapsible callout colors, `inline` admonition placement, `palette.accent`, unmapped and brand icons, abbreviation tooltips, definition-list styling, snippet line ranges and sections (split into files), tag index pages, blog archive/category/pagination pages, anchor-level redirects without the script, cross-page tab memory, git committers, mkdocstrings/click/gen-files/notebook output not carried over, macro functions and Jinja logic, social platforms `footer.socials` lacks, a `linenums` start offset, image sizing from attribute lists, and every plugin you removed without an equivalent.\n",".agents/skills/blume-migrate/references/monorepo.md":"# Migrating into a real repo (monorepos, pnpm, Vercel)\n\nThe framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.\n\n**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.\n\n---\n\n## 1. Non-standard content layout → scope `content.root` + `include`\n\n`content.root` defaults to `\"docs\"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.\n\n**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:\n\n```bash\n# top two path segments of every content file, deduped — your content folders\nfind . -path ./node_modules -prune -o \\( -name '*.md' -o -name '*.mdx' \\) -print \\\n  | sed 's|^\\./||' | cut -d/ -f1 | sort -u\n```\n\nIf that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.\n\n**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: \".\"` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nexport default defineConfig({\n  content: {\n    root: \".\", // content sits directly under this package (apps/docs/), no docs/ subfolder\n    // include/exclude globs are relative to `content.root`. List the actual\n    // content folders so the scan never wanders into src/, public/, etc.\n    include: [\n      \"api/**/*.{md,mdx}\",\n      \"getting-started/**/*.{md,mdx}\",\n      \"guides/**/*.{md,mdx}\",\n    ],\n    exclude: [\"**/node_modules/**\"], // added to the default \"**/_*\" and \"**/.*\"\n  },\n});\n```\n\nSchema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `[\"**/*.{md,mdx}\"]`), **`content.exclude`** (array of globs, relative to `content.root`, added to the default `[\"**/_*\", \"**/.*\"]`; a `!`-prefixed default, `\"!**/_*\"`, takes it back out). Singular `include`/`exclude`, both arrays. A folder `meta.ts` outside every `include` glob is never read (`BLUME_META_OUTSIDE_INCLUDE` names it), so a folder that holds only a `meta.ts`, like an API reference's tag folder, needs a glob of its own (`\"api-reference/**/*.{md,mdx}\"`).\n\nReport to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.\n\n---\n\n## 2. Release-age guards (pnpm `minimumReleaseAge`, Yarn `npmMinimalAgeGate`) block installing fresh Blume\n\npnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.\n\n**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.\n\n**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:\n\n```bash\ngrep -rn \"minimumReleaseAge\\|minimum-release-age\" pnpm-workspace.yaml .npmrc 2>/dev/null\n```\n\n**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):\n\n```yaml\nminimumReleaseAge: 1440\nminimumReleaseAgeExclude:\n  - blume\n```\n\n**Fix — `.npmrc`** form (if the policy lives there instead):\n\n```ini\nminimum-release-age=1440\nminimum-release-age-exclude[]=blume\n```\n\nReport: \"Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else.\"\n\n**Yarn ≥4.10** has the same guard: `npmMinimalAgeGate` in `.yarnrc.yml` (e.g. `2w`). Its per-package exemption is `npmPreapprovedPackages`. Yarn reads rc files from parent directories, so a nested `docs/` package inherits the root's gate. Detect with `grep -rn \"npmMinimalAgeGate\" .yarnrc.yml docs/.yarnrc.yml 2>/dev/null`, then add `npmPreapprovedPackages: [\"blume\"]` (or append `blume` to the existing list) in the file that sets the gate. Don't remove the gate.\n\n---\n\n## 3. Frozen lockfile — regenerate and commit the lockfile in the same change\n\nCI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.\n\n**After every dependency edit, regenerate the lockfile with the repo's package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, `bun install`) and stage it with the manifest.** The pnpm case:\n\n```bash\npnpm install            # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml\ngit add package.json apps/docs/package.json pnpm-lock.yaml\n```\n\nRules:\n\n- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.\n- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.\n- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors \"lockfile not up to date,\" you forgot the plain install.\n\n---\n\n## 4. Vercel monorepo recipe (root-aware install + build)\n\nA plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/` (for a static build; see d) for `vercel()`). `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.\n\nThis is a **copyable template**, not prose. Three pieces:\n\n**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"blume dev\",\n    \"build\": \"blume build\",\n    \"preview\": \"blume preview\"\n  },\n  \"dependencies\": {\n    \"blume\": \"^2\"\n  }\n}\n```\n\n**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"framework\": null,\n  \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\",\n  \"buildCommand\": \"pnpm run build\",\n  \"outputDirectory\": \"dist\"\n}\n```\n\n**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**\n\n- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.\n- **Node version** = 22.19 or newer (Blume requires it). Or pin it in the docs package: `\"engines\": { \"node\": \">=22.19\" }`.\n\nHow the pieces fit, with Root Directory = `apps/docs`:\n\n- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.\n- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.\n- `outputDirectory: \"dist\"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.\n- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.\n\nIf the repo uses **Turborepo**, you can instead build through the root pipeline — `\"buildCommand\": \"cd ../.. && pnpm turbo build --filter=docs\"` — for remote-cache reuse. Keep `outputDirectory: \"dist\"` and Root Directory `apps/docs`.\n\nReport every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.\n\n**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)\n\n**d) Server output (`deployment: vercel()`).** A `vercel()` build (needed for the assistant, the MCP server, Mixedbread search, or the playground proxy) doesn't leave a static `dist/` for Vercel to serve. It writes the Build Output API folder `.vercel/output` in the docs package, which Vercel deploys as is. Use the same `vercel.json` without `outputDirectory`: `{ \"$schema\": …, \"framework\": null, \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\", \"buildCommand\": \"pnpm run build\" }`.\n\n- Keep `framework: null`: it overrides a preset the project already has (Nuxt, Next.js), whose build settings would replace these.\n- Root Directory and Node are the same dashboard settings. Server secrets (`AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token) go in the project's environment variables.\n- With Turborepo, add `\".vercel/output/**\"` to the docs task's `outputs`.\n- The redirect caveat above doesn't apply: `vercel()` writes redirects into `.vercel/output/config.json`, so they're real 3xx responses.\n- `blume preview` can't serve this build; check it with `blume dev` and a preview deployment.\n\n---\n\n## 5. Astro/Vite version mismatch → pnpm patch (manual)\n\nBlume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.\n\n**Detect the risk factor** before it bites:\n\n```bash\ngrep -rn \"\\\"vite\\\"\\|vite@\\| vite:\" pnpm-workspace.yaml package.json 2>/dev/null | grep -i \"override\\|resolution\\|catalog\\|vite\"\n```\n\nBlume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):\n\n```bash\n# 1. Open the offending version in an editable temp dir (use the exact version\n#    pnpm resolved — check `pnpm why astro`):\npnpm patch astro@7.0.6\n\n# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface\n#    (the exact edit depends on the mismatch; the crash's stack trace names the\n#    file and call site). Then commit the patch:\npnpm patch-commit '<the temp path pnpm printed>'\n```\n\n`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  astro@7.0.6: patches/astro@7.0.6.patch\n```\n\nCommit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.\n\nThis is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.\n\n---\n\n## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)\n\nIf the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, titled fences like `:::warning[Heads up]`, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.\n\nThe fix is a committed **pnpm patch** (`patches/oxfmt@0.71.0.patch`), shipped with this skill at `assets/oxfmt@0.71.0.patch` — the same diff Blume's own repo applies, covering titled fences as well as bare ones. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:\n\n```bash\n# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;\n#    <skill> = this skill's directory, the one containing SKILL.md):\nmkdir -p patches\ncp \"<skill>/assets/oxfmt@0.71.0.patch\" patches/oxfmt@0.71.0.patch\n```\n\nThen register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  oxfmt@0.71.0: patches/oxfmt@0.71.0.patch\n```\n\nCommit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.\n\n- **The patch is pinned to `oxfmt@0.71.0`** (the version Blume's own repo pins). pnpm requires an exact version match, and the diff targets a file whose name is hashed per oxfmt release (`dist/markdown-*.js`), so it won't apply to any other version. If `pnpm why oxfmt` reports a different one, pin `oxfmt` to `0.71.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.59 as a peer). If the repo can't take that pin, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.\n- **The risk isn't Ultracite-only.** Any plain `oxfmt` run over Markdown (a lint-staged `oxfmt` on `**/*.md`, say) treats `:::` the same way. Check `lint-staged`/pre-commit config for globs that reach the migrated pages; `.mdx` files outside an `*.md` glob are safe.\n- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.\n\n---\n\n## Checklist for a workspace migration\n\n- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.\n- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.\n- [ ] `npmMinimalAgeGate` present? Added `blume` to `npmPreapprovedPackages` only.\n- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.\n- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22.19+ in the Vercel project.\n- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.\n- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.71.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.\n- [ ] Reported every repo-specific edit and every manual step left to the user.\n",".agents/skills/blume-migrate/references/nextra.md":"# Nextra → Blume\n\nNextra (on Next.js) declares navigation and per-page labels in `_meta` files — and crucially, **a folder's title and its pages' sidebar labels live in the _parent_ `_meta`**. Reconciling that cross-file inheritance is the main task. Nextra 4 was a near-rewrite, so **fingerprint the generation first** — it decides where config lives, where content lives, and what the components are called.\n\n## Detect & fingerprint the generation\n\n- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).\n- **v2/v3 (Pages Router):** content in `pages/`, `_meta.json` (v2) or `_meta.{js,ts}` (v3), a `theme.config.{js,jsx,ts,tsx}`, components from `nextra-theme-docs` (v2) or `nextra/components` (v3).\n- **v4 (App Router):** **no `theme.config`** — config lives as component props in `app/layout.{jsx,tsx}` (`<Layout>`, `<Navbar>`, `<Banner>`, `<Footer>`, `<Search>`). Content is either a **`content/`** directory (with a catch-all `app/[[...mdxPath]]/page.jsx` gateway) **or** Next page files: **`app/**/page.mdx`**.\n- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.\n\n**v4 app-directory repos need a restructure, not a `content.root`.** Pointing `content.root` at `app/` gives every route a trailing `/page` and scans `layout.jsx`/gateway files. Move each `app/foo/page.mdx` → `foo.mdx` (or `foo/index.mdx` when it has children), and exclude the non-MDX app files. The catch-all `page.jsx`, `mdx-components.*`, and `app/layout.*` are teardown artifacts — but **harvest `app/layout.*` first** (see Config).\n\n## Config\n\nNone of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:\n\n- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the footer's repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`; if its origin is not `https://github.com`, set `github.host` to that origin — with `host` omitted, Blume builds the repository and edit links against public GitHub); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer.text` (v2) / `footer.content` (v3) → `footer.copyright` when it's a copyright line: reduce the JSX to plain text, and a `{new Date().getFullYear()}` becomes `${new Date().getFullYear()}` in a template literal; anything else in it → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.\n- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>` (its children are v3's `footer.content`). Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).\n- **`next.config.*`:** harvest **`redirects()`** — static entries and `:param`/`:path*` patterns become Blume `redirects: [{ from, to }]` as written (`permanent: true` → `status: 308`, `false` → `307`); ones with regex params (`:slug(\\d+)`) or `has`/`missing` conditions move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.\n- Root `_meta` entries with **`type: \"page\"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: \"page\"` only maps at the **root**.\n\n## Navigation: `_meta` → `meta.ts` + frontmatter\n\n**Every `_meta` file must be carried over — always convert it to `meta.ts` + frontmatter, never drop it in favor of filename inference.** `_meta` is Nextra's canonical nav source (ordering, labels, folder titles); the filesystem alone can't reproduce it.\n\nFor each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):\n\n| Nextra `_meta` entry | Blume |\n| --- | --- |\n| ordinary page/folder (string or `{ title }`) | slug → parent `meta.ts` `pages` (ordering). A **page** title → that page's frontmatter `sidebar.label`. A **folder** title → that child folder's `meta.ts` `title` (title lives in the parent!). A JSX title → extract its text. |\n| `display: \"hidden\"` | frontmatter `sidebar.hidden: true` |\n| `type: \"separator\"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |\n| `type: \"menu\"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |\n| `href` (external link) | root-level → **`navigation.actions`** (`[{ label, href }]`, plain header links, matching Nextra's navbar placement) or **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route, survives on phones); only drop deep-nested ones (report) |\n| `type: \"page\"` (subfolder, not root) | drop (only root → tabs) |\n| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |\n| `theme: { layout: \"full\" }` on a page | that page's frontmatter `mode: wide` (full column width, no table of contents — the closest match; report) |\n| other `theme` keys (`sidebar: false`, `toc: false`, `typesetting`, …) | drop (report — no per-page switches for them) |\n| `\"*\"` wildcard entry | apply its value as the default for unlisted siblings, then drop |\n\nWrite each folder's `meta.ts` with its `pages` order (from its own `_meta`) and its `title` (inherited from the parent's `_meta`). A v4 **`_meta.global.*`** file holds the whole tree in one place (folder entries carry `items`) — split it into per-folder `meta.ts` files with the same rules.\n\n**Keep the order with `display: \"group\"`.** Nextra draws folders as collapsible rows and keeps pages and folders interleaved as `_meta` lists them. Blume's default `flat` display draws a folder as a plain section header, so it lists each level's pages above its subfolders — a page `_meta` puts after a folder jumps above it. Set `navigation.sidebar.display: \"group\"` (or `display: \"group\"` in the `meta.ts` of every subfolder at a level whose order must hold — one flat sibling folder reorders the level) to keep the authored order in nested folders. Groups then start closed unless they hold the current page, while Nextra opens every folder shallower than its `defaultMenuCollapseLevel` (default `2`: the top-level folders), so add `collapsed: false` to those folders' `meta.ts`. The top level (and each tab's top level) lists loose pages before folders in every display mode, so a root `_meta` that puts a page after a folder can't be matched — report it.\n\n## Frontmatter & titles — synthesize, don't just pass through\n\nMost Nextra pages have **no frontmatter**. Nextra keeps two titles apart: the **sidebar label** falls back `_meta` title → `sidebarTitle` → `title` → first body H1 → filename, while the **heading readers see** is the body H1. Blume renders frontmatter `title` as the page H1, so for **every page**: set frontmatter `title` from the body H1 (else frontmatter `title`, else the `_meta` title, else a humanized filename), **remove the body H1**, and set `sidebar.label` to the `_meta` title (else `sidebarTitle`) wherever it differs from `title`. Don't take the `_meta` label as `title` — that renames the page's heading. `asIndexPage: true` (v4 folder-index marker) → make the file the folder's `index.mdx` and drop the key. Drop any other non-schema key and report it.\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → directive. Types: `default` (the prop default) → `:::note`, `info`→`:::info`, `warning`→`:::warning`, `error`→`:::danger`, `important` (v4) → `:::note`. Bare `<Callout>` → `:::note`. Drop `emoji` (there is no `title` prop to carry — Nextra callouts have none).\n- **v2 flat names:** v2 exported `Tab` and `Card` (flat) from **`nextra-theme-docs`**; v3/v4 use `Tabs.Tab`/`Cards.Card` from `nextra/components`. Handle both: `<Cards>`/`<Cards.Card>`/`<Card>` → `<CardGroup>`/`<Card>`; `<Tabs items={[…]}>` + `<Tabs.Tab>`/`<Tab>` → `<Tabs>`/`<Tab title=\"…\">` (move labels from the parent `items` onto each `<Tab>`). `Cards` `num` → `CardGroup` `cols`.\n- **`<Steps>`:** Nextra's wraps **Markdown headings (h2–h6)**, one step per heading. Convert each heading to a `<Step title=\"…\">` child (Blume `<Steps>`/`<Step>`) and delete the heading — this also keeps step titles out of the TOC, matching Nextra's behavior.\n- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).\n- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.\n- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` pass through in `.mdx`, where Blume renders them as note/tip/warning/note/danger callouts. In a `.md` page they stay plain quotes (`BLUME_MD_GITHUB_ALERT`): rename it to `.mdx`.\n- **Strip or convert every import:** `nextra`, `nextra/*`, `nextra-theme-docs`, `nextra-theme-blog`, plus `next/image` (→ Markdown image or `<Frame>`), `next/link` (→ plain link), and local components (port or inline; report).\n\n## Code fences\n\nNextra's fence meta differs from Blume's — rewrite it: `filename=\"app.js\"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags either one left); line highlighting `{1,4-5}` carries over unchanged; **drop** word-highlight `/word/` and `copy`/`copy=false`. **Keep** inline-code `{:lang}` suffixes (`` `useState(){:js}` ``) — Blume highlights them natively. ` ```sh npm2yarn ` fences → ` ```package-install `.\n\n## Math\n\nNextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume, block math `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field). Convert Nextra's ` ```math ` fences → `$$…$$` blocks (they render as plain code blocks otherwise). Inline math is `$$…$$` too: a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence. MathJax-specific macros → report.\n\n## i18n\n\n- **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → match Blume's `dot` parser as-is, default-locale suffix included: `i18n: { defaultLocale, locales: [{ code, label }], parser: \"dot\" }`, no file moves. Page titles land in each file's own frontmatter, so they stay per-locale, but under `dot` a folder's `meta.ts` serves every locale — build it from the default locale's `_meta` and report translated folder titles from the other locales' `_meta` files.\n- **v4:** every locale has a `content/<lang>/` dir, the default included, and Nextra serves every locale under its prefix (`/en/…`). Blume's `dir` parser takes the other locales' dirs as-is, but the default locale lives **at the content root**: a `content/en/` dir is ordinary content that publishes at `/en/…` and warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`. Move the default locale's files (and `_meta`-derived `meta.ts` files) up one level, and set `i18n.hideDefaultLocalePrefix: false` to keep serving them at `/en/…` as Nextra did — or keep Blume's unprefixed default and add a `redirects` entry per page.\n\n## Package.json & teardown\n\nRepoint `dev`/`build`/`start` scripts to the Blume CLI; remove `next`/`nextra`/`nextra-theme-*` deps and add `blume`. Delete after harvesting: `next.config.*` (redirects first!), `theme.config.*` (v2/v3), `app/layout.*` + the catch-all gateway + `mdx-components.*` (v4).\n\n## Dropped — report these\n\nFooter content other than a copyright line; `primaryHue`-style theming beyond an accent color; `faviconGlyph`; `_meta` separators, menus, `newWindow`; per-page `theme` switches (and `layout: \"full\"` approximated as `mode: wide`); root-level pages `_meta` ordered after a folder; `<Bleed>`; MathJax macros; word-highlight/copy fence meta; any icon you can't reconstruct as a Lucide name.\n",".agents/skills/blume-migrate/references/readme.md":"# ReadMe → Blume\n\nReadMe (readme.com developer hubs) is a hosted platform. Only content reaches Git: settings, the landing page, the glossary, variables, redirects, and every metrics and login feature live in its dashboard. Pages are MDX, or ReadMe's looser \"MDXish\" dialect, saved as `.md`. Published URLs are flat per section (`/docs/<slug>`, `/reference/<slug>`, `/recipes/<slug>`, `/page/<slug>`, `/changelog/<slug>`) however deep a page sits in the sidebar. The plan below keeps every one of them: section folders stay at the top of the content root, categories become `(group)` folders, and nested pages pin their flat URL with `slug`. The API reference is the exception: Blume generates operation pages at new routes, so each old one gets a redirect.\n\nTwo things go wrong with a green build and no warning: emoji blockquote callouts (plain blockquotes) and `:emoji:` shortcodes (literal text). Two more only warn: `doc:`/`ref:` links (dead links; `blume validate` reports `BLUME_UNSUPPORTED_LINK_SCHEME`) and inline `onClick` handlers (they render and do nothing; `BLUME_MDX_EVENT_HANDLER`). The codemod handles the first three; the handlers are yours.\n\n## Detect\n\n| Shape | Tells | The sidebar lives in |\n| --- | --- | --- |\n| **A. Bi-directional Git sync** (ReadMe Refactored) | `_order.yaml` in `docs/`, `reference/`, `recipes/`, `custom_pages/` and their folders; category folders with spaces (`docs/Getting Started/`); `reference/*.json` specs beside endpoint pages with `api: { file, operationId }` frontmatter; `reference/ReadMeConfig/`; `custom_blocks/`; a `main` branch holding only `changelogs/`; branches named after versions (`v3.0`) | folders and `_order.yaml` |\n| **B. `rdme@10` upload** (one-way) | CI running `readmeio/rdme@v10` or `npx rdme@10` with `docs upload`, `reference upload`, `changelog upload`, `custompages upload`, `openapi upload`; frontmatter `category: { uri }`, `parent: { uri }`, `position`, `privacy: { view }`, `content: { excerpt }` (also `rdme docs export` output) | frontmatter |\n| **C. Legacy `rdme` ≤ 9** | CI running `readmeio/rdme@v9` or older (`rdme docs ./dir`, `rdme openapi`, `rdme changelogs`); frontmatter `category: <24-hex id>` or `categorySlug`, `parentDoc` or `parentDocSlug`, `order`, `hidden`, `type: basic \\| link \\| error` | frontmatter, keyed by opaque IDs |\n\nrdme ignores folder layout, so in shapes B and C the folders say nothing about the sidebar. rdme 10 still accepts `rdme changelogs` as a hidden alias, so tell B from C by the rdme version and the frontmatter. A shape A repo can still run `rdme openapi upload` in CI: it's shape A.\n\n**Get everything before you convert.**\n\n- **Shape A** is complete per version branch. Find the branch holding the hub's default version (it can be `v3.0`, not `main`). **Changelogs live only on `main`**: `git archive <main-sha> changelogs | tar -x -C <work-dir>`. Ignore `<version>_<name>` branches (ReadMe's editing branches, kept after they merge).\n- **Shapes B and C** hold only what CI uploads, and drift: editor changes never come back, and a failing upload step stops changes going out. Inventory the live hub, export what the repo lacks, and diff pages against their live source.\n- **Exports.** A Refactored project: connect bi-directional sync to a new, empty repository (Settings → Git Connection), which writes shape A with every version as a branch; a ZIP from the Branch menu (no images); or `npx rdme@10 docs export <dir> --branch <version>` and `rdme reference export <dir>` (shape B frontmatter). A legacy project: `rdme@9` can't export; read pages through API v1 (`GET /api/v1/categories/{slug}/docs`, `GET /api/v1/docs/{slug}`) or upgrade the project. Refactored hubs usually serve a page's Markdown at `<url>.md` (docs.readme.com does), and `/llms.txt`, when it's on, lists every public page or links per-section files that do.\n\n**Go easy on the live hub.** A ReadMe hub can start answering `429` after as few as eight requests in a few minutes, even spaced seconds apart. Spend them on one page of the hub (save it: it carries the settings JSON below), `sitemap.xml`, `llms.txt`, and a few spot checks. The `files.readme.io` image CDN is separate: one run fetched 84 images there at one every half second without a `429`.\n\n## Run the codemod first\n\nFor shape A, on a branch whose name matches no ReadMe version (see Teardown), with the changelogs copied from `main` into the work tree and one page of the hub saved as `hub.html` (skip the `curl` when the user already saved one at the root):\n\n```bash\ncurl -s https://docs.example.com/docs/<any-page> -o hub.html   # settings JSON + glossary\nnode <skill>/scripts/readme-codemod.mjs --site https://docs.example.com --glossary hub.html .          # report only\nnode <skill>/scripts/readme-codemod.mjs --write --site https://docs.example.com --glossary hub.html .  # apply\n```\n\n`--site` (repeatable: add the `*.readme.io` origin too) makes absolute links to the hub root-relative. It's zero-dependency, deterministic, and idempotent (a rerun reports `Nothing to change.`). It:\n\n- moves `custom_pages/` → `page/`, `changelogs/` → `changelog/`, categories → `(Category)` group folders, `.md` → `.mdx`;\n- maps frontmatter (Frontmatter, below) and pins `slug` wherever Blume's route would differ from ReadMe's URL;\n- converts callouts, links, magic blocks, ReadMe components, variables, reusable content, icons, shortcodes, code fences, and headings (Content);\n- writes `_order.yaml` → `meta.ts`, deletes endpoint, tag, and empty parent pages, and turns link pages into redirects (Navigation);\n- writes `reference/overlays/<spec>.overlay.json` (API reference), `readme-redirects.json` (the redirects it can compute), and `readme-migration.json` (every old URL, every deleted endpoint, for the second pass).\n\n**Its report is the to-do list**: per file, what needs judgment (custom components, inline components, `<style>`, event handlers, dead links, unmapped icons), then \"Left for you\". Work through it with the sections below. A second pass after the first build handles endpoint redirects and order (API reference).\n\nShapes B and C: rebuild the tree first (Navigation, shapes B and C), then run the codemod. With no `_order.yaml` it converts page content and frontmatter only: no moves, slug pins, URL inventory, endpoint handling, or redirects (it reports link pages instead).\n\n## Config: the dashboard → `blume.config.ts`\n\nThere's no config file in Git. Every page of a Refactored hub embeds the project's settings as JSON: read them from the saved `hub.html` rather than asking. Map what's set:\n\n| ReadMe (settings JSON) | Blume | Notes |\n| --- | --- | --- |\n| `name` | `title` |  |\n| `logo`, `logo_white_use` | `logo` | download into `public/`. With `logo_white_use: true` the hub showed a white logo on a colored header, invisible in Blume's light header: recolor the white parts to `currentColor`. A bare mark → the string form (`logo: \"/logo.svg\"`); a wordmark → `{ image: \"/logo.svg\", text: \"\" }` (SKILL.md) |\n| `favicon` | `public/favicon.png` (or `.svg`/`.ico`) | picked up by name |\n| `brand.link_color` (`colors.body_highlight`) | `theme.accent` | `_dark` variants → `{ light, dark }` |\n| `brand.primary_color` (`colors.main`) | `theme.action`, or `theme.accent` when no link color | the colored header has no equivalent: report |\n| `brand.theme` | `theme.mode` |  |\n| `typography.headline` / `body` (`Open+Sans:400:sans-serif`) | `theme.fonts.display` / `body` (`{ name: \"Open Sans\" }`) | Typekit → local `variants`, or drop |\n| `nav_names` + enabled `modules` | `navigation.tabs` | one tab per enabled section with content (`{ label: \"Developer hub\", path: \"/docs\" }`) |\n| header links (`link_url` entries) | `navigation.actions` | a sign-up or \"Get API keys\" button → `navigation.cta`; a login link stays in `actions` |\n| `first_page` | `index.mdx` at the root, or a `/` redirect (status 302) | the landing page doesn't sync |\n| `custom_domain` | `deployment.site` | unless the host is Vercel or Netlify (SKILL.md) |\n| Enterprise group path (`/<project>/docs/…`) | `basePath: \"/<project>\"` | one Blume site per child project |\n| `integrations.google.analytics`, `segment`, `heap`; custom JS | `googleAnalytics()`, `segment()`, `heap()`, or `script({ src \\| content })` in `analytics` | these load only in production builds, and wait for `consent` when it's set |\n| `integrations.intercom`, `zendesk` (a support widget readers click) | a `PageFooter` component (`defineComponents`, SKILL.md \"analytics\") that loads it | not `analytics`, which can hold it until consent. Check region settings (an AU Intercom workspace needs `api_base`) |\n| `integrations.google.site_verification`, `<meta>` tags in head HTML | `seo.metatags` | other head HTML: report |\n| `stylesheet_hub2` (custom CSS) | a root `theme.css` | rules for ReadMe's classes (`.rm-*`, `.callout_*`, hashed names) match nothing: carry only rules for the site's own markup |\n| Footer HTML | `footer: { copyright, links }` | a copyright line → `copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`). Report the rest |\n| `variables.defaults` | `variables` | Content. **Defaults can be real credentials** (an API key): never copy one into config |\n| `glossaryTerms` | `--glossary hub.html` | the codemod writes `<Tooltip>`s |\n| `redirects` | `redirects` | URLs and redirects |\n| `error404` | `pages/404.astro` or Blume's default | URLs and redirects |\n| Ask AI, MCP server | `ai.assistant`, `agents.mcp` | need a server-output host adapter: report |\n\nThen the layout. **Leave the section folders where they are.** Set `content.root: \".\"` and scope `include` (`references/monorepo.md` §1), import the codemod's redirects, and add the reference:\n\n```ts\nimport { defineConfig } from \"blume\";\nimport { openapi } from \"blume/reference\";\nimport redirects from \"./readme-redirects.json\";\n\nexport default defineConfig({\n  content: {\n    root: \".\",\n    include: [\n      \"index.mdx\",\n      \"docs/**/*.mdx\",\n      \"reference/**/*.mdx\",\n      \"recipes/**/*.mdx\",\n      \"page/**/*.mdx\",\n      \"changelog/**/*.mdx\",\n    ],\n  },\n  redirects: [...redirects /* , your own */],\n  reference: [openapi({ sources: [/* API reference */] })],\n});\n```\n\n`page/` is ReadMe's custom pages at `/page/<slug>`; Blume's own `pages/` folder is for `.astro` pages, a different thing.\n\n## Navigation (shape A)\n\nWhat the codemod does, so you can check it:\n\n- **Categories → group folders.** `docs/Getting Started/` → `docs/(Getting Started)/`: the group keeps the label and adds no URL segment.\n- **`_order.yaml` → `meta.ts` `pages`**, entries as written (`pages` ignores the parentheses), less duplicates and entries with no file. A category folder's `meta.ts` also sets `title` where Blume would relabel the folder name (`Self-Serve` would show as \"Self Serve\"). A parent page's folder gets `title` from the parent and `display: \"group\"`.\n- **`slug` pins.** ReadMe serves every child flat (`/docs/<child>`; the nested path 404s), so every page below a parent folder gets `slug: docs/<stem>`, and a nested parent's own `index.mdx` takes `docs/<folder>`. A parent folder directly in a category needs none: its folder name is the route. Any stem starting with a digit gets one too, since Blume strips `^\\d+[-_.]` as an ordering prefix (`1-setup` → `/docs/setup`); dates (`12-05-2022`, `2024-01`, `2022-05-12`) and versions (`1.2`) stay whole, so their pins are harmless.\n- **Hidden pages** (`hidden: true`) were left out of the sidebar and search but served by link: `hidden: true` + `search.exclude: true`, URL kept.\n- **Empty parent pages** (frontmatter, no body) redirected to their first child on ReadMe. The codemod deletes them and adds a 302 to the first child; prefer that to keeping one as a `directory: \"card\"` landing page. A parent that stays (with a body, or kept as a landing page) whose child has the same title fails `validate --strict` (`BLUME_NAV_DUPLICATE_LABEL`): retitle one, and report it.\n- **Link pages** (`link: { url }`): deleted, with a 302 to the URL. If the link must stay visible, add it to `navigation.featured`.\n- **Section roots.** `/docs` and `/reference` redirect to the section's first page, as on ReadMe; a typed URL would 404 without it. ReadMe answers them with a 301; the codemod writes a 302, since the first page can change.\n- **Custom pages** had no sidebar: `hidden: true` plus `mode: center` (`fullscreen: true` → `mode: custom`).\n\nLeft for you: an HTML custom page (`custom_pages/<slug>.html`) → `page/<slug>.mdx` with its HTML as JSX, or `pages/page/<slug>.astro` on `PageLayout` for a full document with scripts (`docs/advanced/custom-pages.mdx`); and `/` (see `first_page`).\n\n### Versions\n\nVersion branches hold full copies of guides, recipes, and the reference; ReadMe serves the default version without a prefix and others at `/<version>/docs/<slug>`. **Recommend migrating the default version only**, with `{ from: \"/<version>/:path*\", to: \"/:path*\" }` for each other version and the default's own prefix (`/v3.0/docs/x` redirected to `/docs/x` on ReadMe). Pages that only exist in an old version then 404: report it. To keep old versions, run the codemod on each branch's checkout and copy its `docs/`, `recipes/`, and `reference/` into `<version>/` (`v2.1/docs/…` serves `/v2.1/docs/…`), drop that version's prefix redirect (a pattern over pages fails the build), add `\"<version>/**/*.mdx\"` to `content.include` (a scoped `include` never scans the copy otherwise), and set `versions.archived` (newest first) and `versions.current`. Each id is the URL segment (`v2.1`): ids must start with a letter. References, the changelog, and custom pages stay unversioned, and an archived tree's sidebar isn't scoped by tabs: report both.\n\n## Navigation (shapes B and C)\n\nRebuild the tree from frontmatter into shape A's layout, then run the codemod:\n\n- **Category** → `docs/(<Category title>)/`. Shape B's `category.uri` ends in the title (`/branches/3.0/categories/guides/Getting%20Started`): decode it. Shape C's `category` is an ID: resolve it with `GET /api/v1/categories`, the hub's sidebar, or the user. `categorySlug` names it.\n- **Parent** (`parent.uri`, `parentDoc`, `parentDocSlug`) → a folder named for the parent's slug, holding the parent as `index.mdx`; pin each child's `slug` as in shape A.\n- **Order** (`position`, `order`) → the folder's `meta.ts` `pages`.\n- **Slug**: frontmatter `slug`, else the file name. Name each file after its slug, and pin any that starts with a digit.\n- **Custom pages and changelog entries** → `page/<slug>.md` and `changelog/<slug>.md`, whatever folder CI uploaded them from: the codemod maps their frontmatter by those folder names (`custom_pages/` and `changelogs/` work too), but outside shape A it moves nothing, so the folder is the URL.\n\nThe codemod reports these navigation keys and leaves them: delete them once the tree is built.\n\n## Content\n\n**Every page becomes `.mdx`**: ReadMe's `.md` files are MDX, and in a Blume `.md` page directives stay literal text and components don't render. The codemod converts the mechanical syntax (table below). It also clears what ReadMe's lenient MDXish engine (`\"mdxish\": true` in the settings JSON) accepts and strict MDX rejects: it deletes `<!-- -->` comments, self-closes void tags, and converts `[block:…]` JSON. **It leaves everything that needs judgment, and reports it per file:**\n\n- **Inline components.** A page that defines a component (`export const Name = …`) is left byte for byte. Without hooks it renders as static HTML. With hooks it fails the build: move it to `islands/<Name>.tsx` (`export default`). One defined on several pages goes in `islands/` once.\n- **Custom components** (`custom_blocks/<Name>.mdx`): use a Blume component when it's presentational (a card grid → `CardGroup`), else move it to `islands/<Name>.tsx` and keep `<Name />` in the pages. Blume compiles Tailwind v4 utilities from the project's `.mdx` and `.tsx` files (Tailwind's default theme plus Blume's tokens, `dark:` keyed to Blume's theme), so utility classes keep working; ReadMe's CSS variables (`var(--color-border-default)`) don't exist in Blume: swap them for Blume's (`var(--blume-border)`) or its token utilities (`border-border`, `bg-muted`). Text moved from a prop into children needs `{` and `}` escaped (`\\{`), or the build fails (`BLUME_MDX_UNDEFINED_NAME`, at the brace's line).\n- **Event handlers.** `<button onClick={…}>`, `onMouseEnter`, and friends on inline HTML build green and do nothing; `blume check` and `build` warn `BLUME_MDX_EVENT_HANDLER` at each one. Rebuild the widget as an island (`grep -rnE 'on[A-Z][a-zA-Z]*=' --include='*.mdx'` outside code finds them before a build). Hubs often paste the same widget (a \"contact support\" box) into dozens of pages: one island, and replace every copy. A handler that calls a support widget's global (`Intercom('show')`) also needs that widget loaded (Config, `integrations.intercom`).\n- **`<HTMLBlock>` with `<style>`, `<script>`, or handlers** stays wrapped. **Don't move its `<style>` rules into `theme.css`**: ReadMe authors write global selectors (`p`, `ul`, `h3, h4`), which there restyle the whole site, sidebar included. Rebuild the markup with Blume components (`Tabs`, `Columns`, `CardGroup`, `Panel`, callouts). ReadMe stripped `<script>` unless `runScripts` was on: rebuild what a script did as an island or a `script()` adapter.\n- **Font Awesome `<i>` icons** in raw HTML (`<i className=\"fa fa-plug\" />`): `<Icon icon=\"<lucide>\" />`; Blume loads no Font Awesome, so the `<i>` renders empty. Tailwind classes in raw HTML (`className=\"mt-4 p-4 bg-gray-50\"`) still compile, but fixed colors ignore dark mode: prefer a component (`Panel`, a callout) or Blume's token utilities (`bg-muted`, `border-border`).\n- **Unknown components** (`<PostmanRunButton>`, `<MCPIntro>`, `<TableOfContents>`): drop or replace; each fails the build.\n- **Variables.** `<<name>>`, `{user.name}`, and `<Variable name>` became `{{name}}`; define each in `variables` (from `variables.defaults`). ReadMe filled them per reader; Blume shows one default to everyone (report). The codemod leaves `<<name>>` inside code as written and reports it: change one to `{{name}}` by hand where it should show the default (Blume substitutes in code too).\n- **Glossary terms** with no definition: pass `--glossary`, or write the `<Tooltip>` by hand.\n- **Dead links.** Hubs collect links to slugs from older versions that 404 on the live site too. Check a few live first (ReadMe cross-redirects some, `/reference/x` → `/docs/x`), then point each at the closest current page, or unlink it and keep the text. Report the list.\n- **Images** stay on `files.readme.io`: download them into `public/` and rewrite the URLs (`grep -rhoE 'https://files\\.readme\\.io/[^)\" ]+'`). Some have no extension: add one. Review every alt the codemod derived from a file name.\n- **Tables** with lists or blocks in their cells, or spanning cells (`colSpan`, `rowSpan`), stay `<table>`: check they render.\n- **Recipes**: the codemod deletes the line-highlight comments and demotes the step headings; the walkthrough modal is dropped (report). Repeat a highlighted excerpt under its step with `{1-4}` where it mattered.\n\n| ReadMe | Blume (codemod) |\n| --- | --- |\n| `> 📘 Title` / `>` / `> Body` (emoji first) | `:::info[Title]` … `:::`; 📘 ℹ️ → `info`, 👍 ✅ → `success`, 🚧 ⚠️ → `warning`, ❗️ 🛑 ‼️ ⁉️ → `danger`, any other emoji → `note`. An emoji alone means no title; `#` marks drop |\n| `<Callout icon theme>` | the directive for `theme` (`okay` → success, `warn` → warning, `error` → danger, `default` → note), else for `icon`; a first-child heading becomes the title |\n| `[block:callout]`, `[block:code]`, `[block:image]`, `[block:table]`/`parameters`, `[block:api-header]`, `[block:embed]`, `[block:html]` | the same conversions as their component forms |\n| adjacent fences (ReadMe's code tabs) | `<CodeGroup>`; untitled tabs labeled with the language, as ReadMe showed them |\n| ` ```javascript Node SDK ` | ` ```javascript title=\"Node SDK\" `; `node` → `js`, `curl` → `bash`, `cplusplus` → `cpp`, `objectivec` → `objc` |\n| `<Image src alt caption>`, `<Image …>caption</Image>`, `![1544](url \"image.png\")` | `![alt](src)`, in `<Frame caption>` when captioned; numeric or missing alts derived from the file name; `align`/`width`/`border` drop |\n| `<Embed>` (YouTube, including `google.com/sorry` interstitials), `[x](url \"@embed\")` | `<YouTube id>`; anything else → a link |\n| `<HTMLBlock>{`…`}</HTMLBlock>` | its HTML, inline: comments removed, void tags closed, braces as entities |\n| `<Table>` | a Markdown table: a cell's paragraphs joined with `<br />`, all-empty rows dropped |\n| `<Cards columns>` / `<Card iconColor badge target>` | `<CardGroup cols>` / `<Card color>`, `badge` → `<Badge>`, `target` drops; braces from props escaped |\n| `<Columns layout>`, `<Tab icon>` | `<Columns cols>`, Lucide icons |\n| `<Accordion title>` | a run → one `<Accordion>` of `<AccordionItem>`s; one alone → `<Expandable>`, or a one-item `<Accordion>` when it has an icon (`Expandable` takes none) |\n| `<Glossary>`, `<<glossary:x>>` | `<Tooltip tip>` |\n| `<Anchor>`, `<Recipe>`, `[block:recipe]`, `[block:tutorial-tile]` | a link, `<Card href=\"/recipes/<slug>\">` |\n| `custom_blocks/<Name>.md` used as `<Name />` | `_snippets/<name>.mdx` + `<include>/_snippets/<name>.mdx</include>` |\n| `doc:x`, `ref:x`, `page:x`, `changelog:x`, `blog:x`; `https://<site>/docs/x` | `/docs/x`, `/reference/x`, …; root-relative |\n| Font Awesome (`fad fa-hand-wave`) on frontmatter, cards, tabs, accordions | Lucide, or dropped and reported |\n| `:tada:` | 🎉 (unknown names reported) |\n| a body `# H1` repeating the title; any other body H1 | deleted; every body heading demoted one level |\n\nCallout titles had anchor ids on ReadMe; Blume's don't. Headings otherwise keep their anchors: both slug with github-slugger, so skip `pin-heading-ids.mjs`, which would also crawl the rate-limited hub. One exception: ReadMe's slugger also counted callout titles and the H1 the codemod deletes, so a later heading with the same text was `<slug>-1` on ReadMe and is `<slug>` in Blume. Another: for a heading that ends in an emoji or symbol after a space, github-slugger keeps a trailing dash (`## Features ✨` → `#features-`), which Blume drops (`#features`). `blume validate --strict` flags links to the old id; retarget them.\n\n## Frontmatter\n\nBlume's page schema is strict. The codemod maps:\n\n| ReadMe | Blume |\n| --- | --- |\n| `title` | `title` |\n| `excerpt` (A, C), `content.excerpt` (B) | `description` |\n| `hidden: true` (A), `privacy.view: anyone_with_link` (B) | `hidden: true` + `search.exclude: true` |\n| `hidden: true` (C) | the same, reported: rdme maps it to `privacy.view: anyone_with_link`, but check the live URL and use `draft: true` if it 404s |\n| `deprecated`, `state: deprecated` (B) | `deprecated: true` |\n| `icon`, `appearance.icon` (B) | `icon` (Lucide) |\n| `metadata.title` / `description` / `image` | `seo.title` / `description` / `image`; B's `image: { uri }` is an image id, not a URL: reported |\n| `metadata.keywords` | `search.keywords` |\n| `metadata.robots: noindex`, `allow_crawlers: disabled` (B) | `noindex: true` |\n| `next.pages`, `content.next` (B) | `related`, dropping entries that aren't pages in the repo; Blume takes 10 at most (the rest reported) |\n| `link`, `content.link` (B) | a redirect (Navigation) |\n| `table_of_contents: false` | `mode: wide` (an approximation: report) |\n| `fullscreen` (custom pages) | `mode: custom`, else `mode: center` |\n| `recipe`, `x-import`, `name`, `api`, `api_config`, `recipes` | dropped (`recipes` reported) |\n\nIt keeps keys it doesn't know and reports them: Blume rejects unknown keys, so map or delete each. It drops C's `type` (`basic`, `link`, `error`), since Blume's own `type` sets the content type. Changelog keys are under Changelog.\n\n## API reference\n\nEvery endpoint page is generated from an OpenAPI file. The codemod records each one's old URL, operation, and prose in `readme-migration.json`, and deletes it.\n\n1. **Specs → sources.** One `openapi()` source per spec in `reference/`, labeled with the ReadMe category its endpoints sat under, or the spec's `info.title`: `{ label: \"Connect\", spec: \"./reference/connect.json\" }` serves `/reference/connect`, with operations at `/reference/connect/<tag>/<operation>`. **A category that held several specs** (the report lists them) can't share a label: they'd share a route and the first would win. Give each its own label, or merge the specs first, and report the split. A single spec → `openapi({ spec })`. Add a `navigation.tabs` entry for `/reference`. A YAML spec: convert it to JSON (`npx @redocly/cli bundle x.yaml --output x.json`) and rerun the codemod, which reads JSON only.\n2. **Overlays.** `reference/overlays/<spec>.overlay.json` sets each operation's `description` to its spec text plus the prose its ReadMe endpoint page showed, and converts the ReadMe Markdown inside spec descriptions (emoji callouts, shortcodes, `doc:` links), which ReadMe rendered and Blume would show plain. Add each to its source: `{ label, spec, overlays: [\"./reference/overlays/connect.overlay.json\"] }`. JSX doesn't render in descriptions (`<` and `{` are escaped): rewrite any the report names as Markdown.\n3. **Build, then run the second pass.** List the generated routes, then hand them to the codemod with the same `--spec` pairs (the source's route, then its spec):\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist --spec /reference/connect=reference/connect.json > routes.json\n   node <skill>/scripts/readme-codemod.mjs --routes routes.json --spec /reference/connect=reference/connect.json --write .\n   ```\n\n   It adds a 301 from each old endpoint URL (`/reference/getaccount`) to its operation page, and a 302 from each tag page (`/reference/accounts-1`) to the tag's first endpoint, as ReadMe answered. It skips an old URL that is now a page (`/reference/enrich`, ReadMe's operation, against an Enrich source's overview): a redirect there would hide the page (`BLUME_REDIRECT_MATCHES_PAGE`). It reports each, so you can pick another `route` instead. It adds a 302 from `/reference` to the first endpoint when nothing else claims it, and drops any pass-one redirect that would now hide a reference page. It also rewrites links to old endpoint and tag URLs outside fenced code, dropping anchors (reported), and writes `<route>/<tag-slug>/meta.ts` (`defineMeta({ title, order, pages })`) with ReadMe's endpoint order, where it can differ from the spec's order, which the generated sidebar follows.\n\n4. **Source order, by hand.** Write `<route>/meta.ts` → `defineMeta({ order: <n> })` for each source, with the order the report gives (ReadMe's category order). It merges over the generated group's settings, so the source's `label` stays its title.\n\n- **Conceptual pages** (no `api` key) stay in `reference/(<Category>)/` at `/reference/<slug>` and join the reference tab's sidebar; their group folder's `meta.ts` takes an `order` among the sources.\n- **`reference/ReadMeConfig/`** (`api_config` pages: getting started, authentication, my requests) showed the reader's own API key and request log. The codemod keeps any prose as a page and deletes empty ones: redirect each deleted URL to the closest page.\n- **Hidden endpoints**: remove the operation with an overlay action (`remove: true`) if it should stay unpublished. `x-internal: true` likewise: Blume ignores the flag.\n- **`x-readme` extensions** (and flat spellings like `x-samples-languages`, at the root or on an operation): `samples-languages` → `codeSamples`: `shell`, `node`, `javascript`, `python`, `ruby`, `go`, `php`, `java`, `csharp`, `kotlin`, `swift`, `c`, `cplusplus` (read as `cpp`), and `powershell` carry over as written (so does `curl`, which some specs use); `objectivec`, `http`, `json`, `clojure`, `ocaml`, and `r` have no Blume generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: leave them out and report them. `code-samples` → `x-codeSamples` by overlay (`language` → `lang`, `code` → `source`, `name` → `label`). `explorer-enabled: false` → `playground: false`; `proxy-enabled` (on by default: Try It went through ReadMe's CORS proxy) → `playground: { proxy: true }` on a server-output host, or the API must allow the docs origin (say which). `codeSamples` and `playground` are per `openapi()` adapter, not per source or operation: when specs or operations disagree, take the setting most of them use and report the rest. `samples-enabled` is no longer supported on ReadMe (it hid nothing): ignore it. `headers`, `x-default`, `oauth-options`, `parameter-ordering`, `metrics-enabled`: no equivalent (report).\n- **Versioned references**: unversioned in Blume; an archived version's spec can be its own source with `route: \"/<version>/reference\"` (report).\n\n## Changelog\n\nThe codemod moves `changelogs/<slug>.md` → `changelog/<slug>.mdx` (same URL; digit-led stems pinned with `slug`, which date names no longer need) and adds `type: changelog`:\n\n| ReadMe | Blume |\n| --- | --- |\n| `date`, `published_at` (what synced repos contain), `created_at` (B) | `date` (the day). None (common in C) → reported: take it from the live entry |\n| `author` | `authors`; B's ID object drops |\n| `type` (`added`, `fixed`, `improved`, `deprecated`, `removed`; `none`) | `changelog.category`, capitalized; `none` omitted |\n| `hidden: true`, `privacy.view: anyone_with_link` | `hidden: true`: still built, out of the index and feed |\n\nIt also adds `/changelog.rss` → `/changelog/rss.xml`; the feed needs `deployment.site` or a detected host. Add a `{ label, path: \"/changelog\" }` tab. If the project is open source on GitHub and its releases are the product changelog, offer `githubReleases()` (SKILL.md \"Changelogs\").\n\n## URLs and redirects\n\n**The old URL list**: `readme-migration.json` lists every URL the repo served (hidden pages included) with its kind; add `sitemap.xml` (it omits hidden pages and empty parents), `llms.txt`, other versions (`/<version>/docs/…`), and Enterprise or translated segments (`/<project>/`, `/lang-de/`). At the end every entry must be a page or a redirect to one.\n\n- **Status codes.** Use 302 where ReadMe answered with a 302 (empty parents, tag pages, `/`), for link pages, and for section roots (ReadMe's 301 points at whichever page is first, which can change), and the default 301 for moves (operations). The codemod does, except `/`, which it leaves to you (`first_page`).\n- **ReadMe's own redirects** (`redirects` in the settings JSON, or Settings → Errors & Redirects: `old -> new` per line, full-match regex with `$1`, applied only when no page matches): a literal rule → `{ from, to }`; one-segment or rest-of-path patterns → `:name` / `:name*` (`/old(/\\S*)? -> /new$1` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`, which also sends `/old` to `/new`); anything else → host rules (report).\n- **Versions**: `{ from: \"/<version>/:path*\", to: \"/:path*\" }` (Versions).\n- **The custom 404** (`error404`): rebuild that custom page as `pages/404.astro`, or keep Blume's default. Keep or redirect its `/page/<slug>` URL.\n- **`blume validate`** accepts a redirect `from` as a link target, so rewrite links before trusting it. It warns about a redirect whose `to` leads nowhere (`BLUME_BROKEN_REDIRECT`), but not about loops or chains: run `blume audit --only redirects` after each build. A link to `/llms.txt` or another file Blume generates passes while the config has that feature on.\n- **Check every old URL** against `dist/` and `dist/blume-redirects.json` (no host adapter), following redirects until each lands on a page, or request each from `blume dev` or `blume preview` with `curl -sL`: both answer every redirect, patterns included.\n\n## Teardown\n\n**Disconnect the sync first.** With bi-directional sync on, ReadMe's GitHub app reads and writes every branch named after a version, so restructuring one rewrites the live hub. Migrate on a branch that matches no version (`blume-migration`; `<version>_<name>` would sync), and have the user disconnect the repository (Settings → Git Connection) before merging. Remove every `rdme … upload` step from CI in any shape (and an `rdme` dependency); `rdme openapi validate` can stay as a spec linter.\n\nDelete `custom_blocks/` once converted, `reference/ReadMeConfig/` leftovers, `frontmatter.schema.json`, `hub.html`, `routes.json`, and `readme-migration.json` once the redirects are final (keep `readme-redirects.json`: the config imports it). Scaffold a `package.json` with `blume` and `dev`/`build`/`preview` scripts. Moving the custom domain's CNAME is the user's step: list it.\n\n## Dropped — report these\n\n- **Hosted features**: the editor, Branches and Reviews, the AI Linter and Agent, suggested edits (Blume's edit link comes from `github`), discussions and the dev FAQ, API Metrics, My Developers, My Requests and API logs, the Developer Dashboard, health status updates, and the landing page builder (rebuild as `index.mdx` with `mode: custom`, or `pages/index.astro`).\n- **Personalized docs**: per-reader variables, API keys in Try It, `x-default` credentials, and the `ReadMeConfig` widgets. Blume's Try It takes credentials the reader types.\n- **Login-gated docs** (custom login, JWT, site password, hidden versions): Blume has no sign-in. Point at host-level protection (deployment docs, **Private docs**); it covers the whole site, so public and private pages need two sites.\n- **Changelog options** (layouts, relative dates, author toggle, translations), recipe walkthrough modals, Global Reusable Content across projects, translations through Localize or Transifex (offer `i18n` and `blume translate`), and Enterprise cross-project navigation.\n- **Approximations**: `mode: wide` for `table_of_contents: false`, image sizes and alignment, emoji and unmapped icons, unmapped `x-readme` extensions, unversioned references and changelog, custom components restyled as islands, and HTML or scripts dropped from custom blocks.\n",".agents/skills/blume-migrate/references/redocly.md":"# Redocly → Blume\n\nCovers **Redocly Realm** and the products on the same project format (Redoc, Revel, Reef, alone or combined), usually hosted and edited in **Reunite**. A project is a `redocly.yaml`, `.md` pages written in **Markdoc** (`{% tag %}`, not MDX), optional `sidebars.yaml` files, and API description files in the content tree, each rendered as a reference **at its own path**. The work: Markdoc tags become directives and components, each page's body H1 becomes `title`, sidebars become folders and tabs, and every API operation URL changes shape, so the old ones get generated redirects.\n\n## Detect\n\nA **Realm-family project** has at least one of these (`redocly.yaml` is optional; Realm builds a bare folder of Markdown):\n\n- a `sidebars.yaml`, or a prefixed `<name>.sidebars.yaml`, at the root or in a section folder;\n- `.md` pages using Markdoc tags (`{% admonition`, `{% tabs`, `{% partial`);\n- `@theme/`, `_partials/`, `@l10n/`, or `@<version>/` folders, or `*.page.tsx` React pages;\n- a dependency on `@redocly/realm`, `@redocly/redoc`, `@redocly/revel`, `@redocly/reef`, or a combined package (`@redocly/redoc-revel`, `@redocly/redoc-reef`, `@redocly/revel-reef`). Reunite builds projects with no `package.json`, so a missing one proves nothing.\n\nThree look-alikes:\n\n- **A lint/bundle-only `redocly.yaml`**, with none of the above and only keys Redocly CLI reads (`extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `apis`, `resolve`, `telemetry`), configures `redocly lint`/`bundle`. Leave it untouched and migrate the framework the docs actually use.\n- **The legacy Redocly Developer Portal** (Gatsby; `siteConfig.yaml`, `theme.ts`, a `@redocly/developer-portal` dependency, `.mdx` pages, `*.page.yaml` reference pages, and a `sidebars.yaml` that maps named sidebars to groups of `pages:`). Its content is MDX with `:::` admonitions, so this file's Markdoc mappings don't apply: use SKILL.md's general model, this file's Config, API reference, and URL sections, and tell the user it was the legacy portal.\n- **A standalone Redoc embed** (an HTML page loading `redoc.standalone.js` with `<redoc spec-url>` or `Redoc.init`, or a `redocly build-docs` script): migrate only the spec, with `openapi({ spec })`, and rewrite links to its `#tag/…`/`#operation/…` anchors. A Redoc-product project (`@redocly/redoc` alone) has no Markdown either.\n\n**Keep the content where it is**: `content.root: \".\"`, with `content.include` scoped to the real content folders (`[\"guides/**/*.{md,mdx}\", \"apis/**/*.{md,mdx}\"]`, per `references/monorepo.md` §1), plus each root-level page by name (`\"index.{md,mdx}\"`, `\"changelog.{md,mdx}\"`). A root `*.md` glob would also publish `README.md`, which Realm never did. Routes then keep their paths, `/`-rooted partial paths keep resolving, and the description files stay where `redocly.yaml`'s `apis.*.root` and the lint scripts expect them.\n\n**Realm publishes every `.md` in the project** except the root `README.md` and `public/`, partials folders, `static/`, `@theme/`, and the `ignore` globs, so the old sitemap often lists accidental pages: `.github/pull_request_template`, files under `.claude/`, planning notes. Leave those out of `content.include`, add no redirect for them, and list them in the report so the user can add one if they want full URL coverage. Underscore-prefixed files outside a partials folder were pages in Realm but are excluded by Blume's default `**/_*`: rename any that are real pages (`content.exclude: [\"!**/_*\"]` would publish every partial too).\n\n## Config: `redocly.yaml` → `blume.config.ts`\n\nResolve `$ref`s first (`redirects: { $ref: ./redirects.yaml }` is common). Product folders can hold their own `redocly.yaml`, and `env.<environment>` blocks override per environment: map the production values and report the rest.\n\n| Redocly | Blume | Notes |\n| --- | --- | --- |\n| `seo.title` (`seo.projectTitle` when set) | `title` | Blume appends ` - <title>` to every docs page's `<title>` (not to a page titled the same as the site). Realm added a suffix only with `projectTitle`, so on most Realm sites this is a visible change: report it |\n| `seo.description` | `description` |  |\n| `seo.siteUrl` | `deployment.site` | unless the new host is Vercel or Netlify (auto-detected). Cloudflare Pages exposes only a per-deploy URL, so keep it there |\n| `seo.meta` (`[{ name, content }]`), `seo.keywords` | `seo.metatags` (`{ [name]: content }`, `keywords: \"a, b\"`) | minus the tags Blume writes itself; the build names the setting that owns a refused tag |\n| `seo.lang` | nothing for English; else `i18n: { defaultLocale: \"<code>\", locales: [{ code: \"<code>\", label: \"<language name>\" }] }` | `label` is required |\n| `seo.image`, `seo.jsonLd`, `seo.llmstxt` | **drop** (report) | Blume generates per-page cards, JSON-LD, and `llms.txt`; a page in `llmstxt.excludeFiles` gets `ai.exclude: true` |\n| `logo.image` / `logo.srcSet` (`\"./l.svg light, ./d.svg dark\"`), `altText`, `link` | `logo` / `logo: { image: { light, dark, alt }, href }` | move the files into `public/`; a wordmark also gets `text: \"\"` |\n| `logo.favicon` | **drop the field**: copy the file to `favicon.<ext>` or `icon.<ext>` in the root or `public/` | detected by filename, for `.svg`, `.png`, and `.ico` only: convert any other format |\n| `navbar.items` | `navigation.tabs` / `actions` / `cta` | see Navigation |\n| `footer.items` (columns, each a `{ group, items }` or a single item) | `footer.links` (one row) + `footer.socials` | flatten every column's items in order (`page` → its route). Links to a platform in Blume's `footer.socials` list (`docs/configuration/index.mdx`) become socials; a podcast (Spotify, Apple Podcasts) is `podcast`; others (TikTok) stay links. `copyrightText` → `footer.copyright`. Column labels drop (report) |\n| `redirects` (`'/from': { to, type }`) | `redirects: [{ from, to, status: type }]` | write `from` without a trailing slash; a trailing `/*` carries over as written (`{ from: \"/a/*\", to: \"/b/*\" }`), and also matches the bare `/a`. Blume takes 301/302/307/308 only |\n| `markdown.toc` | `toc: { maxHeadingLevel: <depth> }`, or `toc: false` for `hide` | Realm's default depth 3 is Blume's default |\n| `markdown.lastUpdatedBlock` | `lastModified: \"git\"`, **only when declared** and not `hide: true` | Realm shows \"Last updated\" by default; if the source left it at that, offer `lastModified: \"git\"` in the report. Either way, every page the migration edits (nearly all of them, since each H1 moves) shows the migration commit's date; a rename alone keeps the page's date. Say so |\n| `markdown.editPage.baseUrl` | `github: { owner, repo, branch, dir? }` | the path after the branch → `dir`; a non-`github.com` origin → `host` |\n| `ignore` | `content.exclude: [ … ]` | the globs add to Blume's defaults (`**/_*`, `**/.*`), so `_partials/` stay out of routing |\n| `colorMode` | `theme.mode` | `ignoreDetection` with a first mode of `dark` → `\"dark\"` |\n| `banner` (a list) | `banner: { content, link: { href, text }, dismissible }` | one banner: keep the first unscheduled, untargeted one. `content` is plain text, so a Markdown link in it becomes `link`; colors, targets, schedules drop |\n| `feedback` | `hide: true` → `feedback: false`; `type: comment` → `{ comments: true }` | other types drop |\n| `search` | omit (built-in Orama); `suggestedPages` → `search: { provider: orama(), popular }` (`orama` from `blume/search`) | `engine` and `filters` (facets) drop |\n| `analytics` | `ga.trackingId` (a `G-` id) → `googleAnalytics({ id })`, `gtm.trackingId` → `googleTagManager({ id })`, `amplitude.apiKey` → `amplitude({ key })`, `segment.writeKey` → `segment({ key })`, `heap.appId` → `heap({ id })`, `adobe.scriptUrl` → `adobe({ url })`, `fullstory`/`rudderstack` → `script()` | from `blume/analytics`. A `UA-` id is Universal Analytics, dead since 2023: drop it and say so |\n| `scripts.head` / `scripts.body` | one `script({ src, strategy, attributes })` each in `analytics` | local `./static/x.js` → `/x.js`; `async`/`defer` → `strategy`. **Adapters in `analytics` load only in production builds and wait for consent when `consent` is set.** A script the page needs to work (a support widget, a form embed that buttons call) goes in a `PageFooter` layout slot component instead (`<script is:inline src=\"…\">`), which runs in `blume dev` too. That slot renders on every content page, API operation pages included, but not on `mode: custom` or `frame` pages or custom `pages/`: report where the script is now missing |\n| `links` | a Google Fonts stylesheet → `theme.fonts` (`{ body, display, mono }`) | each role a curated slug, or `{ name: \"Family\" }` for anything else; mind renamed families (Source Sans Pro is `source-sans-3`). `preconnect` links drop |\n| `access` (`requiresLogin`, `rbac`, `sso`, `idps`), root `rbac`/`requiresLogin`/`sso`, `ssoDirect`, `userMenu` | **drop** | Blume has no login: point at host-level protection (deployment docs, **Private docs**), which covers the whole site; report every protected path |\n| `l10n` (older: `i18n`) | `i18n` | see i18n |\n| `apis`, `openapi`, `graphql` | `reference` adapters; **keep `apis` in `redocly.yaml`** | see API reference |\n| `products` (`{ id: { name, folder, icon } }`) | `navigation.selectors: [{ kind: \"product\", label, items: [{ label: name, path: \"/<folder>\" }] }]` |  |\n| `aiAssistant`, `mcp` | nothing | offer `ai.assistant` / `agents.mcp` (server output) as follow-ups |\n| `mockServer`, `catalogClassic`, `entitiesCatalog`, `scorecard*`, `developerOnboarding`, `apiFunctions`, `skills`, `corsProxy`, `responseHeaders`, `reunite`, `removeAttribution`, `palette`, `breadcrumbs`, `codeSnippet`, `navigation`, `navbar.hide`, `footer.hide`, `sidebar.hide` | **drop** (report) | `consent` → a `blume/consent` adapter if the site runs one; `navigation.actions` are Blume's default page actions |\n| `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, `recheck` | **keep in `redocly.yaml`** | see Teardown |\n| `'{{ process.env.X }}'` | `process.env.X` in `blume.config.ts` |  |\n\n## Navigation: `sidebars.yaml` and `navbar` → folders and tabs\n\nWith no sidebar file, Realm generates the sidebar from the file tree (natural sort, `index` first), which Blume's generated sidebar reproduces. Otherwise every file ending in `sidebars.yaml` is a sidebar (one only pulled in through another's `$ref` isn't), `page:` paths resolve from the sidebar file (or the project root with a leading `/`), and **a page no sidebar lists shows no sidebar** (unless its frontmatter `sidebar.path` names one): in Blume's generated tree it would appear. `sidebar.hidden: true` hides it, but also drops it from search, the sitemap, and `llms.txt`, where Realm still listed it: hide only orphans nobody should find, and report which you hid. Realm labels default to the page's `seo.title`, then its first heading, so most `label`s only restate the new `title`.\n\n- **`group` + `items`** → a folder with a `meta.ts` (`title`, and `pages` in the declared order). A group whose pages sit flat beside others, or come from several folders, has no on-disk counterpart: move its pages into a subfolder (a page the group opened becomes its `index`, keeping its URL; the others get redirects) or use an explicit `navigation.sidebar`, per SKILL.md \"Config-declared nesting\".\n- **`label`** → `sidebar.label`, only where it differs from `title`. **`href`** → an explicit sidebar link or `navigation.featured`. **`directory`** → leave the folder to the generated sidebar. **`$ref`** → inline it.\n- **`expanded`**: Realm groups collapse by default, which `navigation.sidebar.display: \"group\"` reproduces. `expanded: true`/`always` → `collapsed: false` in that folder's `meta.ts`. **`menuStyle: drilldown`** → `display: \"page\"`.\n- **`separator`** and **`separatorLine`** have no equivalent: a labeled separator heading a run of pages is usually better as a group; report the rest.\n- **`badges`** → `sidebar.badge` (first name). **`icon`** → Lucide (Icons). `rbac`, `disconnect`, `additionalProps`, `external`, `*TranslationKey` drop.\n- **An API description listed as a `page`** places that API's generated tree. Blume can't put generated operation pages into an explicit `navigation.sidebar`; they sit wherever the source's `route` puts them (see API reference).\n\n**Several sidebars → header tabs.** Realm gives each page the sidebar that lists it, so one sidebar file per section (`guides/sidebars.yaml`), or top-level `tab:` entries in `sidebars.yaml`, is a tabbed site: one `navigation.tabs` entry per section folder (a `tab` with `menu:` → a tab with `items`). The root sidebar's pages, if they share no folder, get a tab with `path: \"/\"` and an `href` to their landing page; Blume picks the longest matching tab path, so section routes still select their own tab.\n\n**`navbar.items`**: an item that opens a section with its own sidebar is that section's tab. The first item into the root sidebar is the root tab; other items into the same sidebar become `navigation.actions`, or drop if they duplicate a tab's target. A \"Sign up\" button → `navigation.cta` with an absolute URL; a GitHub link → `github`.\n\n## Content: Markdoc → MDX\n\nBlume parses `.md` as plain Markdown; directives, components, and ` ```mermaid ` fences need `.mdx`. **Rename only the pages that end up with one of those** (`<include>` works in `.md` too), and leave the rest `.md`, so the diff stays small and their history keeps its file names. Then fix what MDX parses differently, in pages and in every partial an `.mdx` page includes (an included partial parses as MDX):\n\n- **Angle-bracket autolinks** (`<https://…>`, `<team@acme.com>`) fail to compile: write `[text](url)` or a bare URL.\n- A literal `{` or `}` in prose: escape it or put it in inline code. **HTML comments** → `{/* */}`.\n- **Raw HTML** must be JSX-valid: string `style=\"…\"` → `style={{ … }}` or a class in `theme.css`; close void tags (`<br />`). `<figure>` + image + `<figcaption>` reads better as `<Frame caption=\"…\">` around the image. A raw `<iframe>` of a YouTube video → `<YouTube id=\"…\" />`.\n- **File links follow the rename**: Blume resolves `./setup.md` by source file, trying the other extension when that file is gone, so a link to a page renamed `setup.mdx` still lands on it, `slug` and numeric prefix included. Update them anyway: a link to the old name breaks wherever the Markdown is read as files, such as on GitHub.\n\n**Every Markdoc tag must be converted**: in `.md` an unconverted tag ships as literal text with a green build, and `blume check` and `build` only warn `BLUME_TEMPLATE_TAG` at its line (in `.mdx` the page fails to compile, and `blume check` and `validate` report it as `BLUME_MDX_SYNTAX`). Finish with `grep -rn '{%' <content folders>` returning only samples that document Markdoc itself, in code where the warning skips them.\n\n**The body H1 is the title.** Realm renders the page's first `# H1` and uses it for `<title>`, sidebar label, and breadcrumb (unless the page sets `seo.title`; a page with no H1 uses its first heading of any level). Move it to frontmatter `title`, delete it from the body, and demote any later H1 to `##`. Links to the H1's own anchor (`#<h1-slug>`, often the first entry of a hand-written contents list) now break: remove them or point them at the page.\n\n| Markdoc | Blume (`.mdx`) |\n| --- | --- |\n| `{% admonition type=\"info\" name=\"Title\" %}` … `{% /admonition %}` | `:::info[Title]` … `:::` (`success`, `warning`, `danger` keep their names; `idea` → `tip`). No `name` → no brackets |\n| `{% tabs %}` / `{% tab label=\"X\" %}` | `<Tabs>` / `<Tab title=\"X\">`; `tabs id=\"k\"` → `param=\"k\"`. **A group of tabs that each hold only a code fence** → `<CodeGroup>` with titled fences (` ```ts title=\"X\" `); a bare fence directly inside `tabs` was a tab named by its `label` |\n| `{% code-group %}` | `<CodeGroup>` with titled fences; `mode=\"dropdown\"` → `<CodeGroup dropdown>` |\n| `{% partial file=\"…\" variables={a: \"x\"} /%}` | `<include a=\"x\">…</include>` on its own line (Partials) |\n| `{% raw-partial file=\"…\" /%}` | inline the file's text |\n| `{% code-snippet file=\"./x.js\" title=\"x.js\" /%}` | `<include lang=\"js\" meta='title=\"x.js\"'>./x.js</include>` (the file inside `content.root`). `from`/`to`/`before`/`after` ranges: paste the lines into a fence instead (report) |\n| `{% cards columns=N %}` / `{% card title to icon image %}` | `<CardGroup cols={N}>` / `<Card title href icon img>`; `layout=\"horizontal\"` → `horizontal` |\n| `{% accordion-group %}` / `{% accordion title expanded %}` | `<Accordion>` / `<AccordionItem title defaultOpen>` |\n| `{% numbered-list %}` / `{% numbered-item %}` | `<Steps>` / `<Step title>` (an item's leading heading becomes `title`) |\n| `{% img src alt caption /%}` | `![alt](src)`, inside `<Frame caption=\"…\">` when it had a caption. `srcSet` light/dark pairs have no equivalent: keep one image (report) |\n| ` ```mermaid ` fence, `{% diagram type=\"mermaid\" %}` | a ` ```mermaid ` fence (inline the file), in an `.mdx` page. PlantUML and Excalidraw have no renderer: export to SVG (report) |\n| `{% icon name=\"…\" /%}`, `{% inline-svg file /%}` | `<Icon icon=\"<lucide-name>\" />`, `![alt](./file.svg)` |\n| `{% table %}` | a pipe table (raw HTML for `colspan`/`rowspan`) |\n| `{% if %}` / `{% else /%}` | no conditionals: keep the branch the live site showed; per-reader branches (`$rbac`, `$user`) keep the public one (report) |\n| `{% $frontmatter.x %}` / `{% $env.PUBLIC_X %}` | the literal value / `{{public-x}}` with a `variables` entry (`\"public-x\": process.env.PUBLIC_X ?? \"\"`: values must be strings, so an unset variable would fail the config) |\n| `{% json-schema %}`, `{% json-example %}`, `{% openapi-code-sample %}`, `{% openapi-response-sample %}`, `{% replay-openapi %}` | a fenced sample or a `TypeTable`, or a link to the operation page, which renders samples and Try it itself (report) |\n| `{% markdoc-example %}`, `{% login-button %}`, `{% connect-mcp %}`, `{% code-walkthrough %}` | the fence only / drop (report); a walkthrough becomes prose and fences or `<Steps>` |\n| `## Heading {% #id %}` / `{% .class %}` | `## Heading [#id]` / drop |\n| any other tag | a custom tag from `@theme/markdoc/schema.ts`: rewrite its markup, or make its component an island (`islands/<Name>.tsx`, used as `<Name …/>`) |\n\n**Code fences**: Realm reads fence meta only from an annotation: ` ```js {% title=\"app.js\" highlight=\"{2,4-6}\" %} ` → ` ```js title=\"app.js\" {2,4-6} `, and `highlight=\"/word/\"` → `// [!code word:word]`. A bare word after the language was ignored by Realm but becomes a title in Blume: delete stray ones. `[!code highlight|++|--|word|focus|error|warning]` comments carry over.\n\n**Partials** live in `_partials/` folders at any depth, which Blume also keeps out of routing. `markdown.partialsFolders` replaces that default: add the folders it names to `content.exclude` (which keeps Blume's defaults) or rename them with a leading `_`. A leading `/` in `file` meant the project root, which is `content.root` when that's `\".\"`. Variables become lowercase attributes (`favFood` → `favfood`), read in the partial as `{{favfood}}`; a partial that read `$frontmatter` needs the value passed in. A partial used only by `.md` pages can stay `.md`.\n\n## Frontmatter\n\n| Redocly | Blume |\n| --- | --- |\n| body `# H1` | `title` |\n| `title` (top level) | **drop.** Realm never rendered it on Markdown pages (`<title>` was the H1), so moving it to `seo.title` would change live titles. Offer it in the report when it looks like a deliberate SEO title |\n| `description` (top level) | `description` (Realm used the site description instead; this improves the meta description) |\n| `seo.title`, `seo.description`, `seo.image` | the same keys (`seo.title` was also Realm's sidebar label: copy it to `sidebar.label` to keep that) |\n| `seo.keywords` | `search.keywords`, as a list (a string splits on commas). Blume uses them as search terms and writes no per-page `<meta name=\"keywords\">` |\n| `excludeFromSearch: true` | `search.exclude: true` + `ai.exclude: true` (+ `noindex: true` to keep it out of the sitemap, as Realm did) |\n| `slug` | `slug` (a full route); a list → the first as `slug`, the others as redirects |\n| `redirects: { '/old/': {} }` | a config `redirects` entry per path, `from` without the trailing slash. Exact entries beat wildcards, as in Realm |\n| `sidebar: { hide: true }` (hides the sidebar on this page; it doesn't take the page out of one) | `mode: center`. Not `sidebar.hidden`, which removes the page from the navigation, search, and the sitemap |\n| `sidebar: { path }` (which sidebar this page shows) | drop: the tab whose `path` holds the page decides its sidebar |\n| `navigation.nextButton` + `previousButton` `hide: true` | `pagination: false` |\n| `navbar`/`footer` hidden | `mode: custom` (the header only: no sidebar, title, or footer). The header always shows: report a hidden navbar |\n| `rbac` | drop; report the page as access-controlled |\n| `markdown.*`, `codeSnippet`, `breadcrumbs`, `colorMode`, `feedback`, `search`, `versionPicker`, `banner`, `template`, `keywords` (Typesense search curation), `seo.meta`/`jsonLd`/`lang`/`priority` | drop (report what was set) |\n| top-level `editPage` (including legacy `editPage: { disable }`) | drop without reporting: Realm reads only `markdown.editPage`, so it never had an effect |\n| custom keys (`metadata`, …) | `frontmatter.extend` if the site needs them, else drop |\n\n## API reference\n\nRealm renders every description in the content tree (a `.yaml`/`.json` with a top-level `openapi:`, `swagger:`, or `asyncapi:` key, or a `.graphql` schema) **at its path without the extension**: an overview at `/apis/orders/openapi`, a page per tag, and a page per operation below it. `apis` entries match files by `root` and carry lint and display settings; a description needs no entry to render. List every description, and report any `apis.*.root` that points at a missing file.\n\n- **Use `openapi()`** from `blume/reference`, one source per description: like Realm, it gives each operation its own page and URL. (`scalar()` would collapse each API onto one page.) AsyncAPI → `asyncapi()`; GraphQL → `graphql()` with `endpoint`. Leave the files where they are.\n- **Choose each `route`.** When the description shares its folder with guides (`apis/orders/openapi.yaml` beside `apis/orders/quick-start.md`), keep the old path (`route: \"/apis/orders/openapi\"`): its overview URL survives, and the reference sits in that folder's group beside the guides. **When the description is alone in its folder** (`apis/orders/openapi.yaml` and nothing else), the old path nests the reference's group inside a folder group that holds only it: use the folder as the route (`route: \"/apis/orders\"`), so the reference is that group. That moves one overview URL, which the catch-all below covers. Either way, `label` (the `sidebars.yaml` label, else the spec's `info.title`) names the group. Choosing a route inside a topic folder elsewhere moves the reference to where a `sidebars.yaml` had placed it, at the cost of more redirects; say which you chose. Add a `navigation.tabs` entry if an API was a top-level section.\n- **Spec problems fail `blume validate --strict`** (`BLUME_OPENAPI_*` warnings, e.g. path parameters declared `in: query` or missing). Fix them with an OpenAPI Overlay in the source's `overlays`, leaving a generated spec untouched, and report them for a fix at the source.\n- **Decorators don't run.** Realm applies `decorators` (`remove-x-internal`, `filter-out`, `info-override`), and the `preprocessors` and `plugins` behind them, when it bundles a description; Blume reads the raw file. When any are configured, point `spec` at `redocly bundle <api> --output <file>` output or express the change as an Overlay.\n- **Tag labels and order.** Blume reads `x-displayName` for each tag group's label, and orders tags as the spec's `tags` list does and operations as the spec lists them. `x-tagGroups` isn't read: when the spec has them and their flattened order (how Realm ordered tags) differs from `tags`, write `<route>/<tag-slug>/meta.ts` with `defineMeta({ order })`, the tag's position there (the folder under `content.root`, mirroring the route; it must sit in a folder `content.include` reaches, or `BLUME_META_OUTSIDE_INCLUDE` warns that it's never read). It merges over the generated title and order. Get each tag's folder name from Blume's routes (below), not by slugifying. `x-tagGroups` names drop, and tags left out of every group, which Realm hid, now appear: report both. `x-traitTag`, `x-logo`, and `x-badges` are ignored.\n- **Code samples**: `codeSamples.languages[].lang` (root and per-API `openapi:`) → `codeSamples`; Blume reads Redocly's names (`curl`, `JavaScript`, `Node.js`, `Python`, `Java`, `C#`, `PHP`, `Go`, `Ruby`). `R`, `Payload`, `Java8+Apache`, and `C#+Newtonsoft` have no generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: drop them and report. With no languages set, Realm showed its full default list and Blume shows `curl`, `js`, `python`: report the difference. `x-codeSamples` render natively. `codeSamples`, `playground`, and `expandSchemas` are `openapi()` options, shared by all its sources: APIs whose settings differ go in separate `openapi()` entries.\n- **Try it**: `hideReplay: true` → `playground: false`. Realm proxied Try it requests by default; Blume sends them from the browser. Set `playground: { proxy: \"<url>\" }` only for a proxy the user runs; **never carry over a Redocly-hosted one** (`/_api/cors/`, the default; `https://cors.redocly.com`; `https://cors.redoc.ly`). Probe each API's server for CORS with `curl -si -X OPTIONS -H 'Origin: https://<docs-host>' -H 'Access-Control-Request-Method: GET' <server-url>/<some-path>` and list the ones without a matching `access-control-allow-origin` as a follow-up: `playground: { proxy: true }` fixes them once the user picks a server-output host.\n- **Other display options**: `excludeFromSearch` → the source's `includeInSearch: false`; `schemasExpansionLevel: all` → `expandSchemas: true`; `layout`, download buttons, `showExtensions`, `feedback` (Blume's page rating shows on every overview and operation page, with no per-source switch), `mockServer`, and the rest drop (report).\n\n## URLs and redirects\n\n**Save the old URL list first**: a Realm site with `seo.siteUrl` serves `<site>/sitemap.xml`, listing every page **and every API overview, tag, `section/`, and operation page**. Every URL on it must end up a Blume page or a redirect to one.\n\n- **Page routes.** Realm slugs the file path: lowercased, spaces → `-`, `_` and `.` kept, **numeric prefixes kept**. Blume keeps the filename's case and its spaces, and strips a numeric prefix followed by `-`, `_`, or `.` (`01_setup` → `/setup`; a date like `2024-01` or a version stays whole). Rename mixed-case and spaced files to match; a numbered file needs `slug` (or a redirect) to keep its number; a nested `README.md` (Realm `/…/readme`, Blume `/…/README`) becomes `readme.md` to keep its URL.\n- **Moved pages** (materialized groups, `index` promotions, tabs) and **Realm's own redirects** (config, frontmatter `redirects`, extra `slug`s) all become `redirects`. A long generated list reads best in its own `redirects.ts` module, imported into `blume.config.ts`.\n- **Operation pages change shape.** Realm: `<old route>/<tag-slug>/<operationId lowercased>` (`getMultipleDomains` → `getmultipledomains`, `Domains.getDomain` → `domains.getdomain`), or `<old route>/<tag-slug>/paths/<JSON pointer>/<method>` with no `operationId` (`…/paths/~1pets~1%7Bid%7D/get`). Realm tag slugs keep underscores. Blume's slugs differ, so write **one exact redirect per operation**, generated:\n  1. `blume build` once with the references configured.\n  2. `node <skill>/scripts/operation-routes.mjs dist > operation-routes.json` prints, per reference route, every endpoint (`GET /pets/{id}`, or `POST petAdopted` for a webhook) mapped to its Blume route, read from the build itself (a Vercel server build's pages are in `.vercel/output/static`: pass that). `--spec <route>=<file.json>` adds operationIds (convert YAML first: `npx @redocly/cli bundle openapi.yaml --output openapi.json`).\n  3. For each old operation URL, find its operation in the spec: match the last segment against each `operationId` lowercased, or decode the pointer (`%7B` → `{`, `~1` → `/`, `~0` → `~`) to a path plus method. Look up `METHOD path` under the new route, and emit `{ from: <old URL path>, to: <route> }`.\n- **Tag, `section/`, and tag-description heading pages** (`<old route>/<tag>`, `<old route>/section/<heading>`, `<old route>/<tag>/<heading>`) don't exist in Blume: send them to the overview. When the route moved, one catch-all does it: `{ from: \"<old route>/:path*\", to: \"<new route>\" }` (exact operation entries still win). When the route stayed, use `{ from: \"<route>/section/:path*\", to: \"<route>\" }` and `{ from: \"<route>/:tag\", to: \"<route>\" }`, plus exact entries for tag headings; a pattern that covers a page fails the build (`BLUME_REDIRECT_MATCHES_PAGE`).\n- **Legacy hash links** (`#operation/<id>`, `#tag/<tag>`, `#section/…`) worked on Realm, which resolved them in the browser, and still appear **in content, partials, and the description files' own descriptions**. A server can't redirect a fragment. `blume validate` flags one on a page that still exists as a broken anchor, but accepts any redirect `from` as a link target, so it misses the ones whose path now redirects: `grep -rn '#operation/\\|#tag/' <content> <spec files>` and rewrite each to its Blume route. Rewrite absolute links to the site itself (`https://<docs-host>/…`) to root-relative paths too; `blume audit` flags the ones left.\n- **Trailing slashes.** Realm answered `/a/` with a 301 to `/a`, and so do `blume dev` and `blume preview`, for pages and redirect sources alike. In production it depends on the host: static hosts serve a page's `a/index.html` at `/a/`, and server builds (`vercel()`, `node()`) match `/a/` for a redirect `from`, but whether a static host applies a redirect rule to the slashed form varies. When old links used trailing slashes (frontmatter `redirects` often did), check one slashed redirect on a preview deployment and report it.\n- **Versions and locales**: Realm drops the `@` from version folders (`api/@v1/x.md` → `/api/v1/x`) and serves the default version without its segment; non-default locales get the lowercased code (`@l10n/es-ES/` → `/es-es/…`).\n- **Check coverage**: after the build, every old URL should be a page in `dist/` (`<path>/index.html`) or resolve through `dist/blume-redirects.json` (exact entries first, then patterns) to one. That manifest is written for a static build with no host adapter; with one, request each URL from a running `blume dev` (or `blume preview` of a static build) instead: both answer every redirect, patterns included. `blume audit --only redirects` checks the redirects themselves.\n\n## Assets, theme, React pages, and the repo\n\n- **`static/` → `public/`**: Realm serves `static/` at the root, so root-relative references keep working. Drop a Redocly-hosting `robots.txt` (`/~/`, `/cdn-cgi/` rules); Blume writes its own.\n- **`.gitignore`**: Realm projects often list `public/` (and sometimes `_partials`), which would leave moved files uncommitted: remove those lines. Add `.blume/`, `dist/`, and `.env.local`, as `blume init` does.\n- **Node**: Blume needs Node 22.19 or later, and Realm templates often pin 20 in `.nvmrc`. Update `.nvmrc`/`.node-version`, `engines.node`, and CI `node-version`.\n- **`@theme/styles.css`**: the brand color (`--color-primary-base` or the variable it points at) → `theme.accent`; check it against dark mode, which `blume audit` checks for contrast. Rules for the site's own classes (a landing page, figures) move to a root `theme.css`, with `html.dark` selectors rewritten as `[data-theme=\"dark\"]`. Rules for Realm's components don't apply: report them.\n- **`@theme/components/*`**: a component a page uses → an island (`islands/`, or imported by a custom page with `client:*`); an ejected Realm component → the matching layout slot if one exists, else drop. `@theme/layouts`, `Templates`, `plugin.*`, and `markdoc/` drop once their tags are converted.\n- **`*.page.tsx`**: `index.page.tsx` → `pages/index.astro` on `PageLayout`, with interactive parts as React islands, or an `index.mdx` with `mode: custom` when it's mostly links. Its `export const frontmatter` gives the title. `404.page.tsx` → `pages/404.astro`, or Blume's built-in 404. A React-state light/dark image swap becomes two images with CSS hiding one.\n\n## i18n\n\n`l10n: { defaultLocale, locales: [{ code, name }] }` → `i18n: { defaultLocale, locales: [{ code, label: name }] }`. Pages under `@l10n/<code>/` move to `<code>/` at the content root. Blume uses `code` as written in URLs, so write it lowercase (`es-es`, folder too) to keep Realm's URLs. `translations.yaml` label keys → per-locale `meta.ts` titles and tab label maps; UI strings → `i18n.ui` where Blume has one; report the rest.\n\n## Versions\n\nRealm versions a folder through `@<version>/` subfolders and an optional `versions.yaml` (`default`, order; with no `default`, the last listed version is the default); the default version serves without its segment. Blume's versioning is site-wide:\n\n- **Version folders at the content root** → Blume versioning: the default version becomes the current tree (`versions.current: { label }`, required), the others top-level folders listed newest first in `versions.archived` as `{ id, label? }` (ids start with a letter: `@1.0` → `v1.0`, redirect `/1.0/:path*`).\n- **A versioned section** (`api/@v1/`) → plain folders reproduce the URLs: the default version's files move up into `api/`, the others into `api/v1/` (a name like `1.0` stays whole). Add a `navigation.selectors` entry `{ kind: \"version\", … }` and report the missing old-version notice and version-scoped search. Versioned descriptions → one `openapi()` source each.\n\n## Icons\n\nRealm icons are Font Awesome names (`book`, `solid check-circle`, `brands github`) or image paths. Strip the pack prefix and map to Lucide with the table in `references/mintlify.md`; image paths work as written; brand icons other than the few Lucide ships (listed there, `github` among them) have no Lucide match (report).\n\n## Teardown — keep lint and bundle working\n\n1. **Trim `redocly.yaml`, don't delete it**: remove the portal keys and keep `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, and each `apis` entry's `root`, `output`, `overlays`, and lint keys (drop its `openapi`/`graphql`/`theme` display keys). If nothing but `apis` roots remain, keep it anyway when scripts or skills run `redocly lint`; otherwise ask. Run the lint script before and after: the result should match.\n2. Keep `@redocly/cli` and its scripts and CI jobs. Remove the Realm-family packages, `styled-components` and the other template peers nothing else imports (keep `react`/`react-dom` if an island uses them), and the `realm` scripts; add `blume` and the `blume dev`/`build`/`preview` scripts.\n3. Delete `sidebars.yaml` files, `versions.yaml`, `@theme/`, the React pages once rebuilt, `translations.yaml` once mapped, the `tsconfig.json`/`types.d.ts` that only served them, and the redirects file a `$ref` pointed at.\n4. Reunite settings (custom domain, environment variables, teams, hosted analytics) live outside the repo: list what the user must recreate.\n\n## Dropped — report these\n\nLogin, RBAC, and SSO (every protected path); Reunite's editor, reviews, previews, and feedback dashboard; the API explorer's mock server, environments, saved auth, and default CORS proxy (and which APIs now fail CORS); `x-tagGroups` names and the tags they hid; API tag and `section/` pages (redirected); sidebar separators and `expanded: always`; footer column labels; the ` - <title>` suffix now on every `<title>`; dropped frontmatter titles; `seo.image`/`jsonLd` and `llms.txt` sections; extra banners; search facets; resolved `if` branches and per-reader content; `code-snippet` ranges; the Markdoc tags with no equivalent; PlantUML/Excalidraw; light/dark image pairs; unmatched icons; ejected components and Realm CSS; a `UA-` analytics id; scripts that now load only in production; trailing-slash URLs; the accidental pages left out; reset \"last updated\" dates.\n",".agents/skills/blume-migrate/references/starlight.md":"# Starlight → Blume\n\nStarlight is an Astro integration configured via `starlight({…})` in `astro.config.*`, with content in `src/content/docs`. Both are Astro, so this is a natural fit — content can mostly stay in place, with one big caveat: **most Starlight content is `.md`, and Blume's `:::` directives are MDX-only** (see Asides below).\n\n## Detect\n\n- `astro.config.{mjs,mts,ts,js,cjs}` importing and calling **`starlight({…})`** (`import starlight from \"@astrojs/starlight\"`).\n- Content under **`src/content/docs/`**; a `src/content.config.ts` (current) or `src/content/config.ts` (pre-Astro-5).\n- `@astrojs/starlight` dep.\n\nKeep content where it is — set `content.root: \"src/content/docs\"`.\n\n## Config: `starlight({…})` → `blume.config.ts`\n\n**Harvest the surrounding `astro.config.*` too, not just the `starlight()` call:** top-level Astro `redirects` → Blume `redirects`; `site` → `deployment.site`, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md); other integrations → report.\n\n| Starlight option | Blume |\n| --- | --- |\n| `title` | `title` (if per-locale/computed, set manually) |\n| `description` | `description` |\n| `logo` (`{ src }` or `{ light, dark, alt }`) | `logo` — move the referenced file into `public/` |\n| `logo.replacesTitle` | `logo: { text: \"\" }` (renders the mark alone) |\n| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |\n| `social` (array of `{ label, icon, href }`; pre-0.33 legacy: `{ github: url }` object) | derive **`github: { owner, repo }`** from the GitHub entry; every other entry whose platform Blume knows → **`footer.socials`** (`{ discord: href, bluesky: href, … }`; the `x.com`/`twitter` icon → `x`; the platform list is under `footer` in `docs/configuration/index.mdx`); the rest (`mastodon`, `gitlab`, `rss`, …) drop (report) |\n| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it); **an origin other than `https://github.com` → `github.host`** (a GitHub Enterprise repo otherwise links to the public site) |\n| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |\n| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |\n| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |\n| `expressiveCode.themes` | `markdown.code.theme: { light, dark }` — EC assigns by theme _type_, so match each theme by its darkness, not by array position |\n| `expressiveCode.styleOverrides` | drop → restyle via a root `theme.css` file |\n| `head` | per entry: a `meta` tag → **`seo.metatags`** (`{ [attrs.name ?? attrs.property]: attrs.content }` — verification tokens, `theme-color`, and the like), minus the tags Blume writes itself (`description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, `article:*` dates — the build refuses them and names the setting that owns each, e.g. `og:image` → `seo.og`, `twitter:site` → `seo.x.handle`); an analytics `<script>` → one `script()` adapter each in the `analytics` list (from `blume/analytics`): `attrs.src` → `src`, an `async`/`defer` attr → `strategy`, every other `attrs` key → `attributes`, and an inline body (`content`) → `content`; anything else (`link`, `style`, other scripts) → drop and report |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `customCss` | drop → move into a root **`theme.css`** file (auto-picked-up; not a config field) |\n| `components` (overrides) | Blume's layout slots via `defineComponents({ layout: { … } })` in a root `components.ts`: `Header`, `Search`, `Sidebar`, `TableOfContents`, `Pagination` keep their names; `SiteTitle` → `Logo`; **`Footer` → `PageFooter`**, not Blume's `Footer` (that slot is the site footer; Starlight's `Footer` is the page-end last-updated date, pagination, and edit link, which Blume renders itself, so keep only what the override added). Other overrides (`Banner`, `PageTitle`, `Hero`, `EditLink`, `SocialIcons`, …) have no slot — fold their additions into `PageHeader`/`PageFooter`, or `blume eject`; a Starlight name left under `layout` matches no slot and does nothing. Rewrite reads of `Astro.locals.starlightRoute` against the slot's props |\n| `plugins` | **map, don't blanket-drop** — see Plugins below |\n| `pagination: false`, `pagefind`/Pagefind options, `titleDelimiter`, `credits`, `disable404Route`, `routeMiddleware` | drop (report) |\n| `locales` / `defaultLocale` | `i18n` (see below) |\n\n## Navigation: `sidebar`\n\nStarlight's `sidebar` array → prefer letting Blume generate from the filesystem; use `navigation.sidebar` only for shapes files can't express:\n\n- `{ label, items: [{ autogenerate: { directory: \"d\" } }] }` (0.39 and later; before 0.39 the group held it directly, `{ label, autogenerate: { directory: \"d\" } }`) → **structure the folder and rely on filesystem nav** (label → the folder's `meta.ts` `title`; `autogenerate.collapsed` → `collapsed: true` in each subfolder's `meta.ts`). Do **not** map it to a sidebar item with `root:` — in an explicit Blume sidebar that renders a single page link, not the directory's children. Since 0.39 a group's `items` can mix manual entries with an `autogenerate` entry; move those manual pages into the folder (route change → `redirects`) so the folder holds the whole group.\n- string `\"guides/intro\"` → `\"/guides/intro\"`; `{ label?, slug }` → `/<slug>`.\n- `{ label, items: [...] }` → `{ label, items: [...] }` (recursive) — or better, a real folder.\n- `{ label?, link }` → `{ label, href: link }`.\n- `badge` (string or `{ text, variant }`) → `badge` (text only; variant drops). `collapsed` (bool) → `collapsed`. Item `attrs` and `translations` → drop (report).\n\n## Frontmatter\n\n| Starlight | Blume |\n| --- | --- |\n| `title` / `description` / `draft` / `slug` | pass through |\n| `sidebar.{label,order,hidden}` | pass through |\n| `sidebar.badge` | `sidebar.badge` (text only) |\n| `sidebar.attrs` | **remove** (strict schema — build error if left) |\n| `pagefind: false` | `search.exclude: true` |\n| `lastUpdated` (date/string) | `lastModified` |\n| `lastUpdated: false` (boolean) | **remove** (Blume's `lastModified` takes a date; a boolean is a build error) |\n| `prev: false` **and** `next: false` | `pagination: false` (hides both links) |\n| `prev` / `next` otherwise (one side, or a label/link) | **drop** (report — the links can only be hidden together, and their labels come from the linked pages) |\n| `template: splash` | `mode: center` (no sidebar, a wider centered column — the closest layout; report) |\n| `hero` | **no key** — its tagline → a paragraph, its actions → `<Card>`s or links, its image → a Markdown image (report); for a designed landing page, rebuild it as a custom `.astro` page under `content.pages` (`pages/index.astro` on `PageLayout` wins over the content page) |\n| `banner`, `tableOfContents`, `editUrl`, `head` | drop (report) |\n\n## Asides → directives (and the `.md` trap)\n\nStarlight's primary callout syntax is the `:::note`/`:::tip`/`:::caution`/`:::danger` directive **in plain `.md` files**. The directive names map perfectly (Blume aliases `caution`→warning; `error`→danger) and `[Title]` syntax carries over — **but Blume only parses directives in `.mdx`**. In a `.md` file, `:::note` renders as literal text and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. So: **rename every `.md` file that contains asides (or math, or mermaid/package-install fences) to `.mdx`** — for a typical Starlight repo that's most of the content; renaming everything to `.mdx` is usually simpler and safe.\n\n- `<Aside type=\"…\" title=\"…\">` (the component form) → the same directives; bare `<Aside>` → `:::note`.\n- An aside custom icon (`:::tip{icon=\"heart\"}`) → drop the attr (report).\n\n## Components\n\n- **Renames:** `<CardGrid>` → `<CardGroup>`; `<LinkCard>` → `<Card>` (its `description` prop drops — fold into the body); `<TabItem label=\"…\">` → `<Tab title=\"…\">`. `<Tabs>` and `<Card>` stay; **keep** a `<Tabs syncKey=\"…\">` prop as is — Blume's `Tabs` takes `syncKey` with the same scoping (only groups sharing the key switch together). One difference: Blume groups without a key also sync, page-wide, by tab title, where Starlight leaves them independent — add `sync={false}` to a keyless group whose same-titled tabs must stay unlinked.\n- **`<Badge>` needs conversion, not pass-through:** Starlight puts content in a `text` prop and uses variants `note`/`tip`/`caution`/`danger`/`success`/`default` with sizes `small`/`medium`/`large`. Blume's `<Badge>` renders **children** with variants `default`/`accent`/`success`/`warning`/`danger` and sizes `xs`/`sm`/`md`/`lg`. Move `text` into the children; remap variant (`note`→`default`, `tip`→`accent`, `caution`→`warning`, `danger`→`danger`, `success`→`success`) and size (`small`→`sm`, `medium`→`md`, `large`→`lg`).\n- **Convert yourself:** `<Steps>` → Blume `<Steps>`/`<Step>`; `<FileTree>` → Blume `<FileTree>`; `<Code code={…}>` → a fenced code block; `<LinkButton>` → a Markdown link or `<Card>`.\n- Strip `import … from \"@astrojs/starlight/*\"` and `astro:assets` lines.\n\n## Code blocks: Expressive Code meta → Blume\n\nStarlight content is full of Expressive Code fence meta; Blume understands some of it and **promotes unknown bare tokens into the code-block title**, so unconverted meta produces garbage headers. Convert per fence:\n\n- `title=\"file.js\"` → works as-is (or use the space-title shorthand). Line ranges `{2-3}` → work as-is.\n- `ins=`/`del=` line marks → `// [!code ++]` / `// [!code --]` comments; `mark=` → `{ranges}` or `// [!code highlight]`.\n- `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left).\n- `wrap` / `wrap=true` → a bare `wrap` (that block's long lines wrap); a site-wide `expressiveCode.defaultProps.wrap: true` → `markdown.code.wrap: true`.\n- **Drop:** `frame=\"terminal\"`, `collapse=`, `wrap=false`, `preserveIndent`/`hangingIndent`, `\"string\"` and `/regex/` text markers (report if they carried meaning).\n- ` ```diff lang=\"js\" ` → a normal ` ```js ` fence with `[!code ++]`/`[!code --]` markers.\n\n## Plugins — map, don't drop\n\n- `starlight-openapi` → an `openapi({ sources })` entry in Blume's `reference` list, imported from `blume/reference` (delete any generated pages; add the `navigation.tabs` entry).\n- `starlight-blog` → `type: blog` pages (RSS at `/blog/rss.xml`). Blume generates **no** blog index, tag, or author pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag and author pages as dropped.\n- `starlight-versions` → Blume's native versioning, not a `navigation.selectors` dropdown: each archived version's content goes in a top-level folder under `content.root` named for its id, listed in `versions.archived` (newest first), and Blume adds the switcher, the old-version notice, and version-scoped search. Ids must start with a letter (`1.0/` → `v1.0/`, with `redirects` from the old URLs), and a version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as current content. Full reference: `docs/content/versioning.mdx` in the installed package.\n- `starlight-image-zoom` → delete (Blume zooms content images by default).\n- `starlight-links-validator` → delete (`blume validate` covers it).\n- Anything else → report.\n\n## Assets\n\nStarlight co-locates images in `src/assets/` with **relative** references (`../../assets/foo.png`) or `astro:assets` imports; Blume serves `public/` at the site root with absolute URLs. Move `src/assets/*` into `public/`, rewrite relative image paths and `~/`/`@/` aliases to absolute `/…` URLs, and replace `<Image>` imports with Markdown images (or `<Frame>`).\n\n## i18n\n\nStarlight has **two** layouts:\n\n- **`root` locale** (default language at `src/content/docs/`, others in `fr/`, `de/`… subdirs) → matches Blume's `dir` parser as-is: `i18n: { defaultLocale, locales: [{ code, label }] }`, no file moves.\n- **No `root` locale** (every language in a subdir, including the default — `en/…`) → Blume expects the default locale **at the content root**, so move the default locale's files up one level. Starlight served them at `/en/…`, so add a `redirects` entry per page.\n\nDon't restate Blume defaults (`hideDefaultLocalePrefix: true`, `parser: \"dir\"` are already the defaults). Starlight's untranslated-page fallback matches Blume's `fallbackLocale` default.\n\n## Teardown\n\nRemove `@astrojs/starlight` (and plugin deps) from deps, delete the Starlight bits of `astro.config.*` **after harvesting redirects**, delete `src/content.config.ts` / `src/content/config.ts`, repoint scripts to the Blume CLI, add `blume`. A `src/content/docs/404.md` has no direct Blume equivalent — report it (Blume ships its own 404).\n\n## Dropped — report these\n\nSocials on a platform `footer.socials` lacks, badge variants, sidebar/item `attrs` + `translations`, `customCss` beyond `theme.css`, `head` entries other than meta tags and analytics scripts, a page's frontmatter `head`, `routeMiddleware`, splash layouts approximated with `mode: center` and hero blocks rebuilt as content, override components with no Blume slot, starlight-blog's tag and author pages, aside custom icons, EC frames/collapse/text markers, prev/next toggles, unmapped plugins, any `<Icon>` name with no Lucide equivalent.\n",".agents/skills/blume-migrate/references/vitepress.md":"# VitePress → Blume\n\nVitePress is a Vue/Vite static site generator. Its config is a module in `.vitepress/`, navigation is **declared in `themeConfig.sidebar`**, every `.md` page compiles as a **Vue single-file component** (so it can hold Vue components, `{{ }}`, and `<script setup>`), and its Markdown is markdown-it plus VitePress plugins. Watch for five traps:\n\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns (`BLUME_DIRECTIVE_SPACED_NAME`) only for a callout name in `.mdx`. Inside an open `:::` container, the same line closes the container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`). Directives are also MDX-only.\n- **Every page is `.md`, but most need to be `.mdx`.** A page with a callout, component, or math must be renamed, and MDX is stricter than Vue templates.\n- **URLs end in `.html` by default** (`cleanUrls: false`), so every page needs a redirect.\n- **The heading is the body `# H1`, not frontmatter.**\n- **Pages missing from the sidebar are hidden in VitePress.** In Blume they appear unless you hide them.\n\nScope: VitePress 1.x. Notes marked **2.0** cover `2.0.0-alpha.*`.\n\n## Detect\n\n- **A `.vitepress/` directory** holding `config.{js,ts,mjs,mts}` (or `config/index.*`), usually `docs/.vitepress/`. The directory that holds `.vitepress/` is the VitePress **project root**: the argument to `vitepress dev <dir>` in the scripts.\n- **A `vitepress` dependency**, often only in the docs package's `package.json`.\n- **Not VuePress**, which uses `.vuepress/` and `vuepress`/`@vuepress/*` deps (`references/vuepress.md`).\n\n**Where the Blume project goes.** VitePress has a project root, a `srcDir` (the pages, default `.`), and a public directory that is always `<srcDir>/public`. Blume serves `public/` from the directory holding `blume.config.ts`. Two layouts are common:\n\n- The scripts run `vitepress dev docs` from the repo root. Put `blume.config.ts` at the repo root; the default `content.root: \"docs\"` matches (append `srcDir` if one is set). Move `docs/public/` to `public/`.\n- `docs/package.json` runs `vitepress dev`. Put `blume.config.ts` in `docs/`, set `content.root: \".\"`, and scope `include`/`exclude` (`references/monorepo.md` §1). `public/` stays put, but exclude it (VitePress sites keep `.md` there behind `srcExclude`), along with `node_modules`.\n\n## Run the codemod first\n\nBuild the source once (`vitepress build <root>`) and copy `.vitepress/dist` outside the repo (`<old build>` below; skip the build if the user already made one): it's your old URL list, the old heading ids, and the record of what the live site rendered, and Teardown deletes `.vitepress/`. Then, on the VitePress source directory (Blume's `content.root`), dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/vitepress-codemod.mjs <srcDir>          # report only\nnode <skill>/scripts/vitepress-codemod.mjs --write <srcDir>  # apply\nnode <skill>/scripts/vitepress-codemod.mjs --redirects <old build>  # `.html` redirects as JSON\n```\n\nIt's zero-dependency and idempotent. It converts only where the mapping is exact: containers (paired as markdown-it pairs them, see Markdown extensions), code groups, `details`, GitHub alerts, fence `[label]`s and meta, `<<<` snippet imports and `<!--@include:-->` without regions or line ranges, `.html`/`.md`/trailing-slash links backed by a page, `<Badge type text />`, `{#id}`, `[[toc]]`, and the body H1 → `title`. It renames a page to `.mdx` when the result needs MDX (partials keep their names) and makes its comments and `<br>` MDX-safe. **Everything else is a `REVIEW` line**: frontmatter keys Blume rejects, components, Vue syntax, regions and out-of-root snippets, repaired container structure, unconverted includes (including ones inside code fences, which VitePress spliced). Work through that list with the sections below; it's the migration's to-do list.\n\n## Config: `.vitepress/config.*` → `blume.config.ts`\n\nRead the whole module and the helpers it imports (sidebars, nav, per-locale configs). For **2.0**, also read directory-level `config.*` files beside the content.\n\n| VitePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `titleTemplate` | drop (report). Blume's `<title>` is `<page> - <site>`; `seo.title` replaces a page's part |\n| `lang` (not `en-US`) on a single-language site | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`): it sets `<html lang>` and the UI strings, with no switcher or prefix. Use a `code` with a UI pack |\n| `head` | per entry: a known analytics snippet → **that provider's factory** from `blume/analytics` (the gtag loader + `gtag('config', 'G-…')` pair → one `googleAnalytics({ id })`; also GTM, Plausible, …); any other `<script>` → `script()` (`src`, `async`/`defer` → `strategy`, other attrs → `attributes`, inline body → `content`). A `meta` tag → `seo.metatags`, minus the tags Blume writes itself (`description`, `robots`, `og:*`, `twitter:*` card tags; the build names the setting that owns each). `link rel=\"icon\"` → a conventional favicon name in `public/`. Fonts and other tags → drop (report) |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `cleanUrls` | no setting; see \"URLs and redirects\" |\n| `rewrites` | **move each file to the path its rewrite produced**, so the URL stays put (`slug` only where moving is impossible). VitePress relative links are already written against the rewritten paths |\n| `srcDir` / `srcExclude` | `content.root` / `content.exclude`, which adds to the defaults `**/_*` and `**/.*` |\n| `outDir`, `assetsDir`, `cacheDir`, `ignoreDeadLinks`, `metaChunk`, `mpa` | drop |\n| `appearance` | `'dark'`/`'force-dark'` → `theme.mode: \"dark\"`; `false` → `\"light\"`; `true`/`'force-auto'` → omit. Blume always shows the toggle: report a forced mode |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `sitemap.hostname` | `deployment.site`, unless the host is Vercel or Netlify (auto-detected). `sitemap.transformItems` → drop |\n| `markdown.theme` | `markdown.code.theme: { light, dark }`; VitePress's default (`github-light`/`github-dark`) is Blume's, so omit it unless changed |\n| `markdown.lineNumbers: true` | no site-wide switch: add `lineNumbers` per fence, or drop (report) |\n| `markdown.math` | delete `markdown-it-mathjax3`; see Math |\n| `markdown.container` labels, `anchor`, `toc`, `attrs`, `image`, `languages`, `languageAlias`, `headers` | drop (report). A custom `anchor.slugify` changes every anchor |\n| `markdown.config(md)` / `preConfig(md)` with `md.use(…)` | **custom markdown-it plugins: find the syntax each one adds in the content and convert every use** (to Markdown, a directive, or a component); the codemod doesn't know it. Report each plugin |\n| `markdown.codeTransformers` with `@shikijs/vitepress-twoslash` | drop the plugin: ` ```ts twoslash ` works natively |\n| `markdown.externalLinks` | VitePress opens external links in a new tab by default; `markdown.externalLinks: true` keeps that (optional) |\n| `vite`, `vue`, build hooks (`transformHead`, `transformPageData`, `buildEnd`, …) | drop (report what each did). Blume writes canonicals, OG images, `llms.txt`, and the sitemap itself; Astro integrations go in `integrations` |\n| `locales` | `i18n` (see below) |\n\n**`themeConfig`** (default theme):\n\n| `themeConfig` | Blume |\n| --- | --- |\n| `logo` (string, `{ src, alt }`, `{ light, dark, alt }`) | `logo` (string, or `{ image: { light, dark, alt } }`), file in `public/`. A monochrome SVG → `currentColor` and the string form |\n| `siteTitle: false` / a string | `logo: { image: …, text: \"\" }` / `logo.text` |\n| `nav`, `sidebar` | see the Navigation sections |\n| `socialLinks` | GitHub → `github: { owner, repo }` (it renders in the footer); known platforms → `footer.socials` (`twitter` → `x`; list under `footer` in `docs/configuration/index.mdx`); others, including custom `{ svg }` icons → `footer.links`, or drop (report) |\n| `footer` (`message`, `copyright`) | `copyright` → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`); a per-locale `copyright` → a locale map. `message` → drop (report); `footer.links` takes `{ label, href }` |\n| `editLink.pattern` (`…/edit/<branch>/<path>/:path`) | `github: { owner, repo, branch }`. `<path>` leads to the **srcDir**, but Blume builds edit links from the Blume project dir, so `github.dir` is `<path>` minus `content.root` (omit when empty). A non-`github.com` origin → `github.host` |\n| `lastUpdated.formatOptions` | its date fields → `dateFormat`; `text` → drop |\n| `search` | `local` → omit. **Algolia → omit `search`** (Blume's built-in Orama), remove the DocSearch deps, and report the `appId`/`indexName`. That index is almost always DocSearch's, filled by Algolia's crawler, and Blume's sync, given an admin key, replaces the whole index on every build: never point `algolia()` at it. `search: algolia({ appId, apiKey, indexName })` from `blume/search` is for a new index Blume owns, a separate opt-in |\n| `outline` (`2`, `[2, 4]`, `'deep'`, `false`) | `toc` (`{ minHeadingLevel, maxHeadingLevel }`; `'deep'` → 2–6; `false` → `toc: false`), only when declared |\n| `aside`, `docFooter`, the `*Label` strings, `externalLinkIcon`, `carbonAds`, **2.0** `gradedContainers`, theme-package keys | drop. UI strings come from Blume's i18n packs (`i18n.ui`) |\n\n## Navigation: `sidebar`\n\nVitePress shows only what `sidebar` lists, in that order. Make the file tree match it, carrying labels, order, and collapse into `meta.ts`; reach for `navigation.sidebar` only for shapes files can't express.\n\n- **Multiple sidebars** (`{ '/guide/': [...] }`) → one **`navigation.tabs`** entry per key (`{ label, path: \"/guide\" }`, label from the `nav`). Give each tab folder a `meta.ts` `title` matching its tab label. VitePress sidebars rarely list root pages, so once those are hidden, a root route has no loose pages to show and Blume falls back to the full tree, where an untitled tab folder shows its humanized folder name.\n- **A group whose pages sit flat in one folder** (the nesting exists only in config) → **a `(group)` folder**: `docs/(fundamentals)/controllers.md` still serves `/docs/controllers`, so no URL changes and no redirects. Give it a `meta.ts` (`title`, `pages` in the listed order). A listed page from another folder can join with frontmatter `slug` keeping its old route. **Moving a page one folder deeper breaks its relative links and assets** (`./assets/x.png`, `../other.md`): re-base them, or make them root-absolute.\n- **`collapsed`**: unset → not collapsible (Blume's `flat`); `false` → `display: \"group\"` + `collapsed: false`; `true` → `display: \"group\"` + `collapsed: true`, in that folder's `meta.ts`.\n- **A group with its own `link`** → that page becomes the folder's `index` (its URL changes: redirect the old one).\n- **Item `text` that differs from the page's title** → `sidebar.label`. **`base`** → prefix the child links before mapping. External items → `navigation.featured`. `docFooterText`, `rel`, `target` → drop.\n- **Pages no sidebar lists** → `sidebar.hidden: true`. A page listed in two sections, or a cross-section link, can't repeat in filesystem nav: keep one place and report.\n\n## Navigation: `nav`\n\n- A link to a section a sidebar key backs → a `navigation.tabs` entry. **A dropdown whose items mirror that section's sidebar groups** (a mega-menu) → a plain tab: the sidebar already shows them.\n- A dropdown of pages in one section that isn't the sidebar → a tab with `items` (flat; nested groups flatten).\n- External or utility links → `navigation.actions`; a primary call to action → `navigation.cta`. A version dropdown → drop its version links (report), but move its utility links (Releases, Team, Contributing) to `footer.links` or `navigation.actions`, so pages linked only from there stay reachable.\n- `activeMatch`, `target`, `rel`, `noIcon` → drop.\n\n## Titles\n\nVitePress takes a page's heading from its first `# H1`; frontmatter `title` only sets `<title>`. Blume renders frontmatter `title` as the H1, so **a body H1 beside a `title` shows twice**; a page with no `title` takes it from the `# H1` that opens its body and shows it once. The codemod moves a plain H1 into `title` and deletes it. It reports two cases: an H1 with inline markup (move it by hand), and a frontmatter `title` that differs from the H1 (make the H1 `title`, keep the old value as `seo.title`). A home page with no H1 takes `hero.name` / `hero.text`.\n\n## Markdown extensions\n\nVuePress shares the container and fence conventions; its snippet and include syntax differ (see `references/vuepress.md`). **What Blume renders below is MDX-only unless marked `.md` OK.** Of these targets, plain `.md` handles fence titles and meta, `// [!code …]` notations, `{#id}`/`[#id]` anchors, and `<include>`; callouts, components, and math need `.mdx`.\n\n**Container pairing.** markdown-it-container scans each container's lines for its closer, so **a bare `:::` closes the outermost open container whose fence is no longer than it, and everything nested in it**. An unclosed container runs to the end of its parent (the page, for a top-level one), and a `:::` with nothing to close shows as text. Real sites have unclosed code groups that swallow the next callout, closers that close two containers, and stray closers. The codemod pairs this way, ends a code group at its last code block when other content follows inside it (a group only holds code), drops stray closers, gives a nested callout's outer fence the extra colon Blume needs, and reports every repair. Check the old build's HTML for literal `:::` to see what the live site already showed broken.\n\n| VitePress | Blume |\n| --- | --- |\n| `::: tip` / `info` / `warning` / `danger` (spaced) | `:::tip` etc., with **no space**; `::: tip` is never parsed (codemod) |\n| `::: danger STOP` | `:::danger[STOP]`. `:::danger STOP` renders without `STOP`, and `.mdx` warns `BLUME_DIRECTIVE_OPENING_TEXT` (codemod) |\n| `::: note` / `important` / `caution` | **2.0** containers → `:::note`, `:::note`, `:::danger` (`:::warning` with `gradedContainers`). On **1.x** they aren't containers and showed as text; converting changes that: report (codemod) |\n| `::: details Title` (`{open}`) | `<Expandable title=\"Title\">` (`defaultOpen`); untitled → `title=\"Details\"` (codemod) |\n| `> [!NOTE]` `[!TIP]` `[!IMPORTANT]` `[!WARNING]` `[!CAUTION]` (+ `[!INFO]`, `[!DANGER]`) | `:::note` / `tip` / `note` / `warning` / `danger` / `info` / `danger`; text after the marker → `[Title]` (codemod). Unconverted, the five GitHub markers still render as those callouts in `.mdx` (a lazy continuation included); in `.md` they're plain quotes with a literal `[!NOTE]` that warn `BLUME_MD_GITHUB_ALERT` (rename the page to `.mdx`), and `[!INFO]`/`[!DANGER]` are plain quotes in both, with no warning. The codemod leaves lazy-continuation and indented alerts for you |\n| `::: raw`, `::: v-pre` | remove the wrapper lines (codemod) |\n| `::: code-group` + `[label]` fences | `<CodeGroup>`; each block's title is its label (bare token, or `title=\"Two words\"`), else its language as VitePress showed. Blume syncs same-titled tabs page-wide (codemod) |\n| Raw `<div class=\"tip custom-block\">…</div>` (hand-written container markup) | the matching directive |\n| ` ```js{4} ` / ` ```ts:line-numbers ` / ` ```js [file.js] ` | ` ```js {4} ` (Blume reads it glued too) / ` ```ts lineNumbers ` (`=N` start → drop) / ` ```js file.js ` (a bracketed title also shows without its brackets; `.md` OK; codemod) |\n| `// [!code ++]` `--` `focus` `highlight`/`hl` `error` `warning` | same (`.md` OK) |\n| `js-vue` | plain `js`; resolve its `{{ }}` by hand |\n| `<<< @/path/file.ts{2,4} [Label]` (`{lang}`, `:line-numbers`) | `<include meta=\"Label {2,4}\">/path/file.ts</include>` (`lang=\"…\"`, `lineNumbers`); `@` is the srcDir, `/` Blume's content root. In a code group VitePress titles an unlabeled import with its file name. A `.md` target gets `lang=\"md\"`: VitePress showed it as code, Blume would splice it (`.md` OK; codemod) |\n| `<<< file#region` | no region selection in `<include>`: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (selects `#region` markers; set `\"dedent\": true`, since VitePress always dedents regions; runs before every build, so the excerpt keeps tracking the source). Where VitePress couldn't find the region it showed the whole file, so check the old build |\n| a snippet outside the content root (`@/../packages/…`) | `BLUME_INCLUDE_OUTSIDE_ROOT` fails the build: copy the file under `_snippets/` and report |\n| `<!--@include: ./part.md-->` | `<include>./part.md</include>` (`.md` OK; codemod). A line range or region → split the partial. VitePress publishes a partial as a page unless `srcExclude` hides it: prefix partials with `_` (excluded by default) and update the paths. A partial is parsed in the including page's format, so one spliced into `.mdx` must be MDX-safe |\n| `[[toc]]` | delete; Blume has the outline rail (codemod) |\n| `:tada:` emoji shortcodes | paste the emoji. A trailing emoji leaves no dash in the id (`## Emoji 🎉` → `#emoji`); the anchor script pins any old id that differs |\n| `## Heading {#id}` | `[#id]` in both formats; a bare `{#id}` fails `.mdx` (codemod) |\n| markdown-it-attrs (`[link](/x){target=\"_self\"}`, `{.class}`) | delete (codemod, for links) |\n| `$…$` / `$$…$$` (`markdown.math`) | `$$…$$` in `.mdx`, block or inline; convert each inline `$…$`, checking it's math |\n| ` ```mermaid ` (`vitepress-plugin-mermaid`) | as written in `.mdx`; delete the plugin |\n| `:::tabs` + `== Label` (`vitepress-plugin-tabs`) | `<Tabs>` of `<Tab title=\"Label\">`, or `<CodeGroup>` when every tab is one fence |\n| Containers a site registers itself (markdown-it-container in `markdown.config`, **2.0** `customContainers`) | read the registration, then map each to a callout, `<Tabs>`, or a component (report). The codemod pairs them as containers and leaves them as written, lengthening an enclosing callout's fence: Blume ends a `:::` callout at a `::: name` line inside it and drops the line |\n\n## Vue in Markdown\n\nVitePress pages are Vue templates; Blume pages aren't. **In `.mdx` these fail the build**: an HTML comment, `{{ … }}`, a `:prop=` binding, `<script setup>`, an unclosed `<br>`/`<img>`, an `<https://…>` autolink, and `{name}` in prose. **In `.md` they pass through as text or raw HTML** with no error. The codemod reports each.\n\n- **`{{ expr }}`, `$frontmatter`, `$params`** → substitute the value; shared values → Blume `variables`; literal braces → inline code. Vue directives on HTML (`v-if`, `v-for`, `v-pre`) → static content.\n- **`<!-- comments -->`** → `{/* … */}` in `.mdx` (the codemod converts one-line ones).\n- **`<script setup>`, `<script>`, `<style>`** → delete; move styles to a root `theme.css`.\n- **`<Badge type text />`** → `<Badge variant>text</Badge>` (`tip`, the default → `accent`; `info` → `default`; `warning`, `danger` as named; **2.0** `note` → `default`, `important` → `accent`, `caution` → `danger`). Blume leaves a badge's text out of the heading's id (`## Title <Badge>beta</Badge>` → `#title`), as VitePress did for a self-closing badge, but VitePress put a badge's children in it (`<Badge type=\"info\">beta</Badge>` → `#title-beta`), so the codemod pins each converted badge heading to its VitePress id (`[#title-beta]`; `[#title]` for a self-closing badge, redundant but harmless). Unconverted in `.mdx`, a `<Badge type text />` renders empty and warns `BLUME_UNKNOWN_PROP`.\n- **Components** (global ones from the theme's `enhanceApp` or a theme package, and imported `.vue` files): decide per component: a Blume component, Markdown, a static `.astro` component in `components.ts`, or the `.vue` file as an island (`islands/`, with `@astrojs/vue vue` installed; `:users=\"['a']\"` → `users={[\"a\"]}`). Components importing from `vitepress` (`useData`, theme parts) can't run outside it. **First check the old build: a component the theme never registered rendered nothing** (`<!---->` in the HTML), so converting it shows content readers never saw. Report it. Data-driven theme components with no source of data (API lists, contributor avatars) → drop, delete headings left empty, and report the lead-in sentences that now point at nothing.\n- **Raw `<img>`/`<figure>` with a relative `src`** → a Markdown image, which is optimized. Left as is, it works: Blume publishes a file a relative `src` names beside the page, unoptimized, and `blume validate` checks it.\n- **`<ClientOnly>`** → drop the wrapper (an island that needs `window` uses `client: \"only\"`). **`VPTeamMembers`** → a `<CardGroup>` of `<Card>`s, or drop (report).\n- **Data loaders** (`*.data.{js,ts}`) → inline the data, compute it in a custom `.astro` page, or use a content source (report). **Dynamic routes** (`[param].md` + `.paths.*`) → write one page per generated path, then delete the template.\n\n## Home page (`layout: home`)\n\n- **The site home** → `index.mdx` with `mode: custom` (no title or description rendered, so write the heading): the hero as heading, paragraph, and links; each feature as a `<Card title href icon>` in a `<CardGroup cols={3}>` (an image path or inline `<svg>` icon passes to `icon`; an emoji has no slot). For a designed landing page, `pages/index.astro` on `PageLayout`.\n- **Any other page with `layout: home`** (a team or about page using only the hero) → default doc mode: `hero.name` → `title`, `tagline` → a paragraph.\n- Hero CSS variables and theme-package keys (`testimonial`, `features` styling) → drop (report).\n\n## Frontmatter\n\n| VitePress | Blume |\n| --- | --- |\n| `title` | see Titles |\n| `description` | pass through |\n| `head` | `meta name=description` → `description` (when the page has none; VitePress didn't emit a page's `head` description beside a frontmatter one); `og:image` → `seo.image`; `link rel=canonical` → `seo.canonical`; `robots` `noindex` → `noindex: true`; `keywords` → drop; anything else → drop (report) |\n| `layout: doc` | remove |\n| `layout: page` | `mode: frame`. **If the page's only content was a component you're dropping, use the default mode** so the title renders |\n| `layout: home` | see Home page |\n| `layout: false` / a custom layout | `mode: custom`, or a custom `.astro` page (report) |\n| `sidebar: false` | `mode: center` |\n| `aside: false` / `outline: false` | `mode: wide`, or drop; `outline` levels, `aside: left` → drop (report) |\n| `prev: false` **and** `next: false` | `pagination: false`; other `prev`/`next` → drop (report) |\n| `lastUpdated: <date>` | `lastModified`; `false` → remove |\n| `titleTemplate`, `editLink`, `footer`, `navbar`, `pageClass`, `markdownStyles`, theme-package keys | drop (report), or declare with `frontmatter.extend` if a component you wrote reads them |\n\n## Icons\n\nVitePress has no page-icon system. Icons you add (tabs, `meta.ts`, cards) are kebab-case Lucide names; an inline Lucide SVG (`class=\"lucide lucide-server\"`) maps to that name (`server`).\n\n## Assets\n\n- **`<srcDir>/public/`** → `public/` beside `blume.config.ts`; root-absolute references keep working.\n- **Relative images** work as written and are optimized, **except draw.io/diagrams.net SVG exports**: the `content=\"<mxfile…>\"` attribute on their `<svg>` hides the size Astro reads, so Blume serves the file as it is and warns `BLUME_SVG_UNOPTIMIZED` at the line that embeds it. Strip that attribute to have it optimized.\n- **`.html` files under `public/`** (`/pure.html`) → keep the links.\n\n## i18n\n\nA `root` locale with others in `fr/`, `zh/` folders matches Blume's `dir` layout: `i18n: { defaultLocale, locales: [{ code, label }] }`, `code` = the folder (not `lang`), `defaultLocale` = the root's language (`en-US` → `en`). Per-locale nav labels → label maps on `navigation.tabs`/`actions`; per-locale sidebars → each locale's `meta.ts`; per-locale `title`/`description` → drop (report). Pin translated headings to their own old ids: translated pages keep their own old anchors, and the anchor script (see URLs and redirects) pairs each page with its own old page, never with the default locale's. With every locale in a folder (`en/` too), move the default up to the root and redirect `/en/…`, or set `hideDefaultLocalePrefix: false`.\n\n## URLs and redirects\n\nThe old URL list is `<old build>/**/*.html` (or the live `sitemap.xml`). Blume serves `/guide/foo` for `guide/foo.md` and `/guide` for `guide/index.md`.\n\n- **`cleanUrls: false` (the default): one exact redirect per non-index page,** `{ from: \"/guide/foo.html\", to: \"/guide/foo\" }`. `vitepress-codemod.mjs --redirects <old build>` prints them all; paste the array into `redirects`. A pattern can't express a `.html` suffix (`/:path*.html` fails validation).\n- **Don't redirect an index URL** (`/guide/`, `/guide/index.html`, `/index.html`): static hosts serve Blume's `guide/index.html` there already, and `blume dev`/`preview` redirect `/guide/` to `/guide`. `from: \"/guide/\"` takes over the page (`BLUME_REDIRECT_MATCHES_PAGE`); `from: \"/guide/index.html\"` builds (no redirect page is written over the page) but is unneeded.\n- **`cleanUrls: true`:** old URLs already match.\n- **A moved route** (undone `rewrites`, a renamed `_`-prefixed page, a numeric prefix Blume strips, a `foo.md` beside `foo/index.md`, which is one route in Blume: `BLUME_DUPLICATE_ROUTE`) → redirect both the `.html` and the clean spelling.\n- **Internal links:** the codemod rewrites `.html`, absolute `.md`, and trailing-slash links that a page backs; it reports the rest. `blume validate` reports each `.html` link left (`BLUME_BROKEN_ASSET`, naming the page's route when one matches). Fix links in config, frontmatter (`hero.actions`), and raw `<a href>` too.\n- **Anchors.** VitePress slugs headings its own way (punctuation runs → one `-`, leading digit → `_`); Blume uses `github-slugger` (punctuation dropped). On a large site hundreds of ids change (`#what-s-new` → `#whats-new`, `#tsed-core` → `#tsedcore`). After the first build, run the anchor script against the old build: `node <skill>/scripts/pin-heading-ids.mjs --old <old build>` (`--write` to apply), then rebuild and rerun it until it reports 0. By default it pins every id that changed; `--only-linked` pins only the ids content links to, for a site where hundreds change, and is a choice to report, since outside deep links to the rest then land at the top of the page. It reads the ids VitePress published, so a custom `markdown.anchor.slugify` is covered too, and it finds each old page at its own route, so add `--map` only for pages that moved. A `NOTE` names a heading it couldn't pin: pin it by hand (`## What's new [#what-s-new]`) if the heading still exists. A pin **replaces** the generated id, so run `blume validate --strict` after: any `BLUME_BROKEN_ANCHOR` left was already dead on the old site (retarget it or drop the fragment, and report it).\n- **Verify** in `dist/`: every old URL is either a built page (`dist/<route>/index.html`) or a `from` in `dist/_redirects` / `dist/vercel.json` whose target is a built page. Static builds also write a meta-refresh page at `dist/<route>.html/index.html`. `blume preview` serves both, applies the redirects as the host files do, and redirects `/<route>/` to `/<route>`, so you can click through old URLs there. On Netlify, set `deployment: netlify({ output: \"static\" })`: only its `_redirects` forces the rules, so a build for no named host gets the meta-refresh page with a 200. How GitHub Pages serves `/<route>.html` is unverified, so check a few old URLs after deploying there.\n\n## Teardown\n\n- Remove `vitepress`, its plugins (`vitepress-plugin-*`, `@shikijs/vitepress-twoslash`, `markdown-it-*`), the theme package, and `vue` (keep `vue` + `@astrojs/vue` for Vue islands). Delete `.vitepress/` (once its `dist/` is copied out), data loaders, `.paths.*` files, and PostCSS/Tailwind configs that only served the theme. Theme CSS worth keeping → a root `theme.css` against Blume's tokens (`--vp-c-brand-1` → `theme.accent`).\n- **Check the `tsconfig.json` in the Blume project dir**: Blume's build reads it, so drop `.vitepress` includes and make sure `extends` resolves from that dir (install the root workspace first), or delete it if it only served VitePress. An unresolvable `extends` fails the build (`BLUME_TSCONFIG_EXTENDS`, at the line that names it).\n- Repoint `docs:dev`/`docs:build`/`docs:preview` to `blume dev`/`build`/`preview`. Grep the repo for `.vitepress/dist` and `vitepress build`: CI and deploy configs (Pages workflows, publish scripts, `netlify.toml`) must now publish `dist/` and install the docs package's deps. `.gitignore`: `.vitepress/cache`, `.vitepress/dist` → `.blume`, `dist`.\n- A `404.md` → delete (Blume ships a 404; `pages/404.astro` replaces it).\n\n## Dropped — report these\n\n`titleTemplate`, the footer `message` (and any link inside `copyright`), forced `appearance`, per-page `outline`/`aside`, one-sided `prev`/`next`, page-level toggles (`editLink`, `footer`, `navbar`, `pageClass`), `head` entries beyond meta tags and analytics, build hooks, Vite and custom markdown-it plugins, unsupported social links, line-number offsets and the site-wide switch, snippet regions and include ranges (now split files), copied out-of-root snippets, data-driven and unregistered theme components (and any content they hid), `VPTeamMembers`, data loaders and dynamic routes, hero styling, the DocSearch index (with its credentials), old-site anchors that were already broken (and any left unpinned under `--only-linked`), version-dropdown links, and `::: note`/`important`/`caution` text that 1.x showed literally.\n",".agents/skills/blume-migrate/references/vuepress.md":"# VuePress → Blume\n\nVuePress is the Vue static site generator that VitePress succeeded. Its config is a module in `.vuepress/`, navigation is **declared in the theme's `sidebar`**, every `.md` page compiles as a **Vue single-file component**, and its Markdown is markdown-it with `::: name` containers. Most of that layer matches VitePress, so **this file covers only what differs, and sends you to a named section of `references/vitepress.md` for the rest. Keep that file open too.** Watch for six traps:\n\n- **`README.md` is a folder's index page in VuePress, but an ordinary page in Blume.** `guide/README.md` publishes at `/guide/README`, and the folder URL `/guide` has no page. Rename every `README.md` to `index.md` (or `.mdx`), then fix the links to it (see \"URLs and redirects\").\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns only for a callout name in `.mdx`; inside an open `:::` container the line closes it instead. See vitepress.md § \"Markdown extensions\".\n- **URLs end in `.html`** (`/guide/getting-started.html`). Every non-index page needs a redirect.\n- **v1 sidebar groups are collapsible unless `collapsable: false`** (note the spelling). That's the opposite of VitePress and of VuePress 2.\n- **A page's title is its frontmatter `title`, falling back to the body `# H1`, and a page often has both, with different text.** See \"Titles\".\n- **Theme components fail silently.** `<code-group>`, `<code-block>`, and `<router-link>` have hyphenated names, so they pass through as unknown HTML elements, in `.mdx` too. A code group renders every block stacked, and a router link renders with no `href`. Neither `build` nor `validate` reports them.\n\nScope: **VuePress 1** (`vuepress@1.x`, Vue 2, webpack) with the default theme or a theme that `extend`s it, in full. **VuePress 2** (`vuepress@2.0.0-rc.*`, Vue 3, `@vuepress/theme-default` 2.x) is covered in notes marked **v2**. `vuepress-theme-hope` (v2) has its common syntax under \"Themes\". For any other theme (`vuepress-theme-reco`, `vuepress-theme-plume`, an ejected `.vuepress/theme/`), inventory its components and frontmatter by hand. The v1 mappings that VuePress's own docs exercise were run end to end on them; the rest of v1 (`permalink`, `base`, `vuepress-plugin-tabs`, the blog plugin, …), the **v2** notes, and the hope section come from source and docs and haven't been run.\n\n## Detect\n\n- **A `.vuepress/` directory** holding `config.{js,ts}`, or v1's `config.yml`/`config.toml`, or v2's `config.mjs`. It's usually `docs/.vuepress/`. **v2** also reads `vuepress.config.{ts,js,mjs}` from the directory the CLI runs in. The directory that holds `.vuepress/` is the **source dir**: the argument to `vuepress dev <dir>` in the `package.json` scripts.\n- **Dependencies:** `vuepress` (both majors), the v2 betas' `vuepress-vite`/`vuepress-webpack`, or `@vuepress/*`, `vuepress-plugin-*`, and `vuepress-theme-*` packages. They're often only in the docs package's own `package.json`.\n- **Which major:** a `vuepress` version of `^1` means v1. A `2.0.0-*` version, `defineUserConfig`, `bundler: viteBundler()`, or `theme: defaultTheme({…})` means v2. A v1 config is CommonJS (`module.exports`) or `defineConfig` from `vuepress/config` (1.9+), with the theme's options under `themeConfig`. A function config (`ctx => ({…})`) may branch on `ctx.isProd`, so read both sides.\n- **Not VitePress.** VitePress uses `.vitepress/` and a `vitepress` dependency (`references/vitepress.md`).\n\n**Where the Blume project goes:** `blume.config.ts` goes in the directory the docs scripts run in (the `package.json` holding `vuepress build docs`, which may be a workspace package like `packages/docs`, not the repo root), with `content.root` set to the source dir (the default `docs` usually matches). When the scripts run `vuepress build` inside the source dir itself, use vitepress.md § \"Detect\"'s second layout. The public directory is `<source dir>/.vuepress/public` (in **v2**, the `public` option). Move it to `public/` beside `blume.config.ts`. Blume's default `exclude` (`**/.*`) already skips `.vuepress/`.\n\n## The VitePress codemod\n\nvitepress.md's codemod (`<skill>/scripts/vitepress-codemod.mjs`) also reads VuePress 1 pages. Over VuePress's own docs, with `README.md` already renamed to `index.md`, it converted containers, `details`, `v-pre`, comments, `<Badge>`s (pinning their heading anchors to the old ids, which Blume now gives them anyway), `[[toc]]`, glued fence ranges, body H1s, and links that a page backs, and listed much of the rest as `REVIEW` lines. Run it on the source dir **after** the `README.md` renames (before them, links to folder indexes come back as unbacked; it leaves `.vuepress/` alone), then correct what it treats as VitePress:\n\n- **Not reported at all:** lowercase and hyphenated tags (`<code-group>`, `<code-block>`, `<router-link>`, `<demo-1>`; `<Foo-Bar/>` is reported as `<Foo>`), `:emoji:` shortcodes, `@flowstart` blocks, `sidebar: auto` (which then fails the build), **v2** `@[code]` imports, and relative `README.md` links, which stay as written. Grep for each, and rewrite the `README.md` links to the folder (see \"URLs and redirects\").\n- **`<code-group>` and `<code-block>`** aren't converted, and a page with nothing else to convert stays `.md` (VuePress's English `guide/getting-started.md` did). Convert them, and rename the page to `.mdx`. **v2** `::: code-tabs` / `::: tabs` come back as unknown containers to convert by hand.\n- **The v2 betas' `:::: code-group` + `::: code-group-item Title`** is read as VitePress's `::: code-group`: it comes out as an empty `<CodeGroup>` followed by the items, with a removed \"stray\" closer. Convert those blocks by hand before running it.\n- **v1 badge types** `error`, `warn`, and `yellow` come out as `accent`: map them as \"Vue in Markdown\" says.\n- **A frontmatter `title` that differs from the H1** is reported for `seo.title`. In VuePress it was the sidebar text, so it goes to `sidebar.label` (see \"Titles\").\n- **`<<< @/…`** resolves `@` against the source dir, which is wrong when `vuepress` ran elsewhere. Check each import it converts or reports against the `<<<` row below.\n- **`--redirects`** reads only VitePress builds: on a VuePress build it prints `[]`, which doesn't mean no redirects are needed. Use the command in \"URLs and redirects\".\n\n## Config: `.vuepress/config.*` → `blume.config.ts`\n\nRead the whole config, including the modules it imports. VuePress sites often keep `nav` and `sidebar` in per-locale files under `.vuepress/config/`.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `head` | same as VitePress `head` (vitepress.md § \"Config\"); the `[tag, attrs, content]` format is identical. `link rel=\"apple-touch-icon\"` → copy the file to `public/apple-touch-icon.png`, which Blume detects. `manifest` and `mask-icon` links → drop with the PWA |\n| **v2** `lang` | same as VitePress `lang` |\n| `locales` | `i18n` (see \"i18n\") |\n| `dest`, `temp`, `cache`, `host`, `port`, `open`, `shouldPrefetch`, `shouldPreload`, v1 `devTemplate` / `ssrTemplate`, **v2** `templateDev` / `templateBuild`, `evergreen`, `extraWatchFiles`, `debug` | drop |\n| `patterns` / **v2** `pagePatterns` | `content.include` / `exclude`. v1's default pattern also builds `.vue` files as pages: rebuild each as Markdown or a custom `.astro` page (report) |\n| `permalink` (v1, site-wide) / **v2** `permalinkPattern` | no setting. Give each affected page a `slug` that reproduces its old URL (see \"URLs and redirects\") |\n| **v2** `public` | the directory to move to `public/` |\n| `theme` | see \"Themes\" |\n| `markdown.lineNumbers` (v1) | no site-wide switch. Add `lineNumbers` to the fences that need it, or drop it (report) |\n| `markdown.slugify` | a custom slugify changes every anchor. See vitepress.md § \"URLs and redirects\" |\n| `markdown.anchor`, `markdown.toc`, `markdown.pageSuffix`, `markdown.extractHeaders` / **v2** `markdown.headers` | drop. If `extractHeaders` added `h4`, set `toc: { maxHeadingLevel: 4 }` |\n| `markdown.externalLinks` / **v2** `markdown.links.externalAttrs` | VuePress opens external links in a new tab by default. To keep that, set `markdown.externalLinks: true` (optional) |\n| `markdown.extendMarkdown`, `chainMarkdown`, `markdown.plugins` / **v2** `extendsMarkdown` (also inside local plugins) | custom markdown-it plugins. Find the syntax each adds and convert every use, as in vitepress.md's `markdown.config` row. Report each plugin |\n| **v2** `markdown.importCode.handleImportPath` | aliases for `@[code]` paths. Apply them as you convert those imports |\n| `configureWebpack`, `chainWebpack`, `postcss`, `stylus`, `scss`, `sass`, `less`, `alias`, `define`, **v2** `bundler` | drop (report) |\n\n**Theme options** (v1 `themeConfig`; **v2** the `defaultTheme({…})` options):\n\n| VuePress | Blume |\n| --- | --- |\n| `logo` | `logo`. Move the file into `public/`. **v2** `logoDark` → `logo: { image: { light, dark } }`, and `logoAlt` → `alt` |\n| `nav` / **v2** `navbar` | see \"Navigation: `nav`\". `navbar: false` → drop (report) |\n| `sidebar` | see \"Navigation: `sidebar`\" |\n| `sidebarDepth`, `displayAllHeaders`, `activeHeaderLinks` | drop. VuePress listed a page's headings in the sidebar; Blume lists them in the outline rail (`h2`–`h3`, set by `toc`) |\n| `repo` (`owner/name` means GitHub) | `github` adds the footer mark **and** an Edit on GitHub action, so set it only where the site had edit links: v1 with `editLinks: true`, **v2** unless `editLink: false`. Otherwise keep just the mark: `navigation.repo: \"https://github.com/<owner>/<name>\"`. A GitLab or Bitbucket URL → a `navigation.actions` link; edit links drop (report) |\n| `docsRepo` / `docsBranch` / `docsDir` (edit links) | `github: { owner, repo, branch, dir }`, from `docsRepo` when it's set (then point `navigation.repo` at `repo`). `docsBranch` defaults to `master` in v1 and `main` in **v2**, so set `branch` to the real branch. `docsDir` is the repo path to the source dir, so `github.dir` is `docsDir` minus `content.root` (`packages/docs/docs` → `packages/docs`). **v2** `editLinkPattern` → read the path from it the same way |\n| `lastUpdated` | v1 turns the date on when `lastUpdated` (a label or `true`) is set in `themeConfig` **or in any `themeConfig.locales[path]`**: either → `lastModified: \"git\"`, and the label drops. **v2** shows it unless `lastUpdated: false`; map it only when the config sets it, and report the default otherwise |\n| `nextLinks: false` **and** `prevLinks: false` (v1) | `pagination: false` on every page: there's no site-wide switch. Only one of them → drop (report) |\n| `search: false` | `search: false` |\n| `algolia` (`apiKey`, `indexName`, `appId`), in `themeConfig` or, in v1, any `themeConfig.locales[path]` | omit `search` (Orama) and report the credentials. A v1 `algolia` block is almost always **DocSearch**: Algolia's crawler fills the index (the app is `BH4D9OD16A` when `appId` is missing, and `algoliaOptions.facetFilters` often marks a shared index). Its records aren't Blume's, and Blume's sync, given an admin key, replaces the whole index on every build. Never point `algolia()` at it; a new index of your own is a separate opt-in |\n| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` → report: Blume always shows the toggle |\n| **v2** `hostname` | `deployment.site`, unless the target host is Vercel or Netlify |\n| `locales` | see \"i18n\" |\n| `smoothScroll`, `searchMaxSuggestions`, `searchPlaceholder`, `repoLabel`, `editLinkText`, **v2** `contributors`, `externalLinkIcon`, `home`, `notFound`, `backToHome`, and the other UI labels | drop. A translated UI string → `i18n.ui` |\n\n**Plugins** (`plugins`, plus the ones the theme turns on):\n\n| VuePress | Blume |\n| --- | --- |\n| `@vuepress/plugin-search` (v1's built-in search), **v2** `search`, `slimsearch`, `flexsearch`, `orama` | omit: Orama is the default |\n| **v2** `@vuepress/plugin-meilisearch` | omit, and report: Blume has no Meilisearch adapter |\n| **v2** `@vuepress/plugin-docsearch` | as the `algolia` row: omit `search` and report |\n| `@vuepress/plugin-google-analytics` (v1 `{ ga }`, **v2** `{ id }`) | `analytics: [googleAnalytics({ id })]` from `blume/analytics`. A `UA-` id stopped collecting when Google retired Universal Analytics, so ask for the GA4 `G-` id, or drop it (report) |\n| Other analytics plugins | the matching `blume/analytics` adapter, or `script()` |\n| `vuepress-plugin-sitemap`, **v2** `@vuepress/plugin-sitemap` / `plugin-seo` (`hostname`) | `deployment.site` (with the same host rule as above). Blume writes the sitemap, canonicals, and Open Graph tags itself |\n| `@vuepress/plugin-medium-zoom` | omit: Blume content images zoom on click by default |\n| `@vuepress/plugin-back-to-top` | omit: Blume's page actions include \"Scroll to top\" |\n| `@vuepress/plugin-pwa` | drop (report). Blume has no service worker. The `manifest` `head` link drops with it |\n| `@vuepress/plugin-last-updated` | `lastModified: \"git\"` |\n| `nprogress`, `active-header-links`, `smooth-scroll`, **v2** `copy-code`, `git` | drop |\n| `@vuepress/plugin-register-components` | see \"Vue in Markdown\" |\n| `container` (`vuepress-plugin-container`) / **v2** `@vuepress/plugin-markdown-container` | containers the site registers. Read each entry's `type`, `before`, `after`, and `defaultTitle`, then map it as in vitepress.md's last \"Markdown extensions\" row (report) |\n| **v2** `@vuepress/plugin-redirect` | its `config` map → `redirects`. `redirectFrom` frontmatter → one `redirects` entry per old path, pointing at that page. A page with `redirectTo` → delete it and add a redirect |\n| `@vuepress/plugin-blog`, `vuepress-plugin-blog` | `type: blog` pages plus a hand-written index (see `docs/advanced/blog.mdx`). Tag, category, and pagination pages → drop (report) |\n| Mermaid, math, tabs, flowchart, and include plugins | see \"Markdown extensions\" |\n\n**Styles:** v1's `.vuepress/styles/palette.styl` `$accentColor` (or **v2** `--vp-c-accent` in `.vuepress/styles/index.scss`) → `theme.accent`. Move other rules worth keeping from `index.styl` / `index.scss` to a root `theme.css`, written against Blume's tokens (report).\n\n## Navigation: `sidebar`\n\nThe rules in vitepress.md § \"Navigation: `sidebar`\" apply: multiple sidebars become tabs, a group whose pages sit flat beside other groups' pages becomes a **`(group)` folder** (`guide/(advanced)/frontmatter.md` still serves `/guide/frontmatter`, so no redirects; list it in the parent `meta.ts` `pages` by its bare name, `advanced`), and pages no sidebar lists need a decision. The item formats differ:\n\n- **Multiple sidebars** (`sidebar: { '/guide/': [...], '/': [...] }`, fallback key last) → one `navigation.tabs` entry per non-root key, as in VitePress. Items inside a key are relative to it: `''` is the key's `README.md`, and `'getting-started'` is `/guide/getting-started.html`.\n- **Items** are page paths, with `.md` optional, or `[path, 'Text']`. A `Text` that differs from the page's title → that page's `sidebar.label`.\n- **v1 groups** (`{ title, path, collapsable, sidebarDepth, children, initialOpenGroupIndex }`) → a folder (or `(group)` folder) with a `meta.ts` (`title`, `pages` in order). `path` → the folder's `index` page. `collapsable` unset or `true` → `display: \"group\"`. Blume's groups then start collapsed except the one holding the current page, as in v1 (which also opened the first group, or `initialOpenGroupIndex`, on a page no group held). When every group collapses, set `navigation.sidebar.display: \"group\"` once. `collapsable: false` → keep the default `flat`. `sidebarDepth` and `initialOpenGroupIndex` → drop.\n- **v2 groups** (`{ text, link, prefix, collapsible, children }`, where strings are file paths such as `'foo.md'` or `'/bar/README.md'`, resolved against `prefix`) → the same, except `collapsible: true` is the only collapsible case.\n- **`'auto'`** (v1, as a key's value or the whole `sidebar`) and **v2** `'heading'` (the default) showed only the current page's headings. Blume shows the page tree instead, with the headings in the outline rail. Report that the sidebar changed.\n- **Pages reachable only from the navbar** (common: `config/`, `faq/`, `miscellaneous/` with `sidebar: auto`) → give each section a place in the tree: a tab for a top-level navbar link, or a folder for a dropdown group. Use `sidebar.hidden: true` only for pages that were truly unlisted.\n\n## Navigation: `nav`\n\nMap it as in vitepress.md § \"Navigation: `nav`\". v1 items are `{ text, link }`, or `{ text, items }` for a dropdown; a dropdown can hold `{ text, items }` subgroups, which flatten. **v2** items are `{ text, link }`, `{ text, children, prefix }`, or a page path string. Strip `.md` and `.html` from internal `link`s, and turn `README.md` into its folder. `ariaLabel`, `target`, `rel`, and `activeMatch` → drop.\n\n**A dropdown whose pages span several folders** (VuePress's own \"Learn More\": `api/`, `faq/`, `miscellaneous/`, and an external changelog) fits no single tab. Make it a tab with `path: \"/\"` and `items` for the pages (on those routes the sidebar lists the folders no other tab claims, which here are exactly those, though the tab also highlights on the home page), and move its external links to `navigation.actions`. Or, for a few pages, use `navigation.featured`.\n\n## Titles\n\nVuePress names a page with its frontmatter `title`, falling back to a heading on the page's first line (normally the `# H1`). That name is the sidebar text and the `<title>`, while the H1 still renders as written. v1's `metaTitle` overrides the whole `<title>`. Blume renders frontmatter `title` as the H1. On every page:\n\n1. Move the H1 text into `title` and delete the H1 line. Keep only the text of an H1 that carries a link or a `<Badge>`: move the link into the body, and the badge to `sidebar.badge`.\n2. A frontmatter `title` that differs from the H1 → `sidebar.label`, since it was the sidebar text.\n3. `metaTitle` → `seo.title`, minus VuePress's ` | Site` suffix: Blume adds ` - <site title>` itself. A `metaTitle` without that suffix was the whole `<title>`: keep it as written and report that the site name is now appended.\n\nA page with no H1 and no `title` took the site title. Give it a real one. **VuePress gave the H1 an id, but Blume's title has none**, so a link to it (`using-a-plugin.html#using-a-plugin`, meaning the top of the page) fails `validate` with `BLUME_BROKEN_ANCHOR`: drop the fragment.\n\n## Markdown extensions\n\nEverything Blume renders from these is MDX-only unless vitepress.md marks it `.md` OK, so a page that needs a callout or component is renamed to `.mdx`.\n\n| VuePress | Blume |\n| --- | --- |\n| `::: tip` / `::: warning` / `::: danger` (v1 default theme). **v2** adds `info`, `note`, `important`, and `caution` | same as VitePress's containers, with the space removed and a title moved into `[Title]`. **v2** `important` → `:::note` (Blume's alias), `caution` → `:::danger`. v1 has no `note` or `info` container: unless a plugin registers one, the live site showed those lines as text, so converting them changes the page (report) |\n| `::: details Title` | same as VitePress: `<Expandable title=\"Title\">`, or `title=\"Details\"` when untitled. VuePress has no `{open}` |\n| `::: v-pre` | same as VitePress's `::: raw` row |\n| `::: slot name` (v1 Markdown slots) | no equivalent. Put the content where the layout showed it, or drop it (a home page's `::: slot footer` → see \"Home page\") (report) |\n| `<code-group>` + `<code-block title=\"YARN\">` (v1, 1.6+) | `<CodeGroup>` with blank lines inside, each block's `title` becoming its fence's title (` ```bash YARN `, or `title=\"Two words\"`). `active` on a later block → move that block first (Blume opens the first tab), or use `<Tabs defaultTabIndex={n}>` with one fence per `<Tab title>`. Inside a list item, indent `<CodeGroup>`, its fences, and the closing tag to the item's content column, or the list breaks. Unconverted, the blocks render stacked with no warning |\n| `<CodeGroup>` + `<CodeGroupItem title>`, or `:::: code-group` + `::: code-group-item Title` (**v2** betas) | the same. Unconverted in `.mdx`, `CodeGroupItem` fails the build, and the spaced containers show as text |\n| `::: code-tabs#id` + `@tab Label` / `@tab:active Label` (**v2** default theme, hope) | `<CodeGroup>`, each `@tab` label becoming the title of the fence that follows it (VuePress rendered only that first fence). `#id` → drop: Blume already syncs same-titled tabs across the page. `:active` → as for `<code-block active>` |\n| `::: tabs#id` + `@tab Label` (**v2**, hope) | `<Tabs>` of `<Tab title=\"Label\">`. `#id` → `syncKey=\"id\"`, `@tab:active` → `defaultTabIndex={n}` (zero-based), and `@tab Label#value` → keep only `Label` |\n| `:::: tabs` + `::: tab \"Label\"` (`vuepress-plugin-tabs`, v1) | `<Tabs>` of `<Tab title=\"Label\">` |\n| ` ```js{4} ` | same as VitePress: Blume reads it as written, and the codemod adds the space |\n| **v2** ` ```ts title=\"config.ts\" ` | works as written (`.md` OK) |\n| **v2** `:line-numbers`, `:line-numbers=N`, `:no-line-numbers` | the first two as in VitePress; `:no-line-numbers` → remove. **v2** turns line numbers on for every block by default; Blume has no site-wide switch (report). `:collapsed-lines`, `:v-pre`, `:no-v-pre` → remove |\n| **v2** `// [!code …]` (the `notation*` options) | same as VitePress |\n| `<<< @/path/file.js{2}` / `<<< @/file.js#region` (v1) | `<include>`, mapped as in VitePress's `<<<` rows, but **`@` is the directory `vuepress` ran in, not the source dir.** Running `vuepress build docs` from the repo root makes `@/docs/x.js` → `<include>/x.js</include>`. v1 imports take no `[label]` or `{lang}`. A region, or a file outside the content root → split or copy the file, as VitePress's rows say |\n| **v2** `@[code](./file.js)` / `@[code{3-10} js{2}:no-line-numbers](../file.js)` | `<include lang=\"js\" meta=\"{2}\">../file.js</include>`. Paths are relative to the page, like `<include>`'s. A line range (`{3-10}`) → split the file (report). Unconverted, it renders as `@` plus a link to the file, with no error |\n| **v2** `<!-- @include: ./part.md -->`, with `{start-end}` or `#region` (`@vuepress/plugin-markdown-include`, md-enhance, hope) | same as VitePress's `<!--@include:-->` row. Unconverted in `.md` it stays an HTML comment, so the content disappears silently. In `.mdx`, it fails the build |\n| `[[toc]]`, `:emoji:` | same as VitePress. VuePress left an emoji shortcode out of a heading's anchor (`## Emoji :tada:` → `#emoji`), and Blume's `## Emoji 🎉` is `#emoji` too, so the pasted emoji keeps the id |\n| `@flowstart` … `@flowend` (`vuepress-plugin-flowchart`) | renders as literal text. Redraw it as a ` ```mermaid ` flowchart (`.mdx`) or an image (report) |\n| **v2** `> [!NOTE]` GitHub alerts (with the hint plugin's `alert` on, or in hope) | same as VitePress's alerts row |\n| Math, ` ```mermaid `, markdown-it-attrs (`{#id}`, `{.class}`) | same as VitePress's rows. v1 and v2 core have no `{#id}` or attrs; only plugins (hope's `attrs`) add them |\n\n## Vue in Markdown\n\nvitepress.md § \"Vue in Markdown\" applies: the `.mdx` build errors, `{{ }}`, `<script>`/`<style>` blocks, comments, `<ClientOnly>`, directives, and the per-component decision. VuePress adds:\n\n- **Globals:** `$page`, `$site`, `$frontmatter`, `$themeConfig`, `$lang`, `$localePath`, `$title`, `$description`, and `$style` (from `<style module>`) → substitute or delete. `$withBase('/img.png')` → the plain root path (`![alt](/img.png)`): Blume adds `deployment.base` to internal links and assets.\n- **`<Badge>`** → Blume `<Badge>`, with the text as **children**. v1 `type`: `tip` (the default) or `green` → `accent`; `warning`, `warn`, or `yellow` → `warning`; `error` → `danger`. **v2** `type`s map as in vitepress.md. `vertical` → drop. An unconverted badge renders empty in `.mdx`, which warns `BLUME_UNKNOWN_PROP`, and as nothing in `.md`, with no warning. **Anchors:** VuePress left a badge out of a heading's anchor (`## Custom Containers <Badge text=\"default theme\"/>` → `#custom-containers`), and so does Blume, so the converted heading keeps its id.\n- **`<router-link to=\"/x.html\">`** (v1), `<RouterLink>`, and **v2** `<RouteLink>` → a Markdown link to the route (`[text](/x)`). `<OutboundLink/>` and `<Content/>` → delete.\n- **Global components:** v1 registers every `.vuepress/components/**/*.vue` file globally, named after the path (`Foo/Bar.vue` → `<Foo-Bar/>`, `demo-1.vue` → `<demo-1/>`), plus theme `global-components` and `Vue.component()` calls in `.vuepress/enhanceApp.js`. **v2** registers them through `@vuepress/plugin-register-components` or `.vuepress/client.{js,ts}`. Decide each as in VitePress. A hyphenated tag passes through as an unknown HTML element, so give the Blume component a PascalCase name and rename its tags (`<demo-1/>` → `<Demo1 />`). **v1 components are Vue 2**, but `@astrojs/vue` runs Vue 3, so an island needs a Vue 3 port: functional components, filters, `$listeners`, `.sync`, and `this.$set` all changed (report). A component that reads `$page`, `$site`, `$themeConfig`, or `this.$router` can't run outside VuePress: rewrite it or drop it.\n- **An escaped `\\<App>`** (VuePress's `api/node` headings) renders as text in `.mdx`, with no warning, so it can stay as written.\n- **`.vuepress/enhanceApp.js` / v2 `client.{js,ts}`** (Vue plugins, router hooks, global components) → drop, and report what each did. **v2** client-config `layouts` → custom `.astro` pages.\n\n## Home page (`home: true`)\n\nRebuild it as in vitepress.md § \"Home page (`layout: home`)\": `index.mdx` with `mode: custom`, or a designed `pages/index.astro`. It's usually the source dir's `README.md` (or `index.md`). VuePress's keys:\n\n- v1: `heroImage`, `heroAlt`, `heroText` (it defaults to the site title, and `null` hides it), `tagline` (it defaults to the site description, and `null` hides it), `actionText` + `actionLink` (one button), `features: [{ title, details }]`, and `footer`. **v2:** `actions: [{ text, link, type }]` replaces the single action, and adds `heroImageDark`, `heroHeight`, and `footerHtml` (which renders `footer` as HTML).\n- `mode: custom` is full-bleed with no padding, so a hero image renders huge and flush left. Wrap the hero in a centered block (`<div style={{ maxWidth: \"60rem\", margin: \"0 auto\", padding: \"3rem 1.5rem 0\", textAlign: \"center\" }}>`, with blank lines inside so the Markdown renders) and cap the image (`style={{ maxHeight: \"280px\", margin: \"0 auto\" }}`). Add `sidebar.hidden: true` so the home page isn't a sidebar row.\n- Features have no icon or link: each becomes a `<Card title>` with `details` as the body, in a `<CardGroup cols={3}>`. Features written as raw HTML in the body (`<div class=\"features\">`) map the same way.\n- `footer` (HTML with **v2** `footerHtml: true`) or a `::: slot footer` is usually a copyright or license line (\"MIT Licensed | Copyright © 2018-present …\") → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); move its links into `footer.links`, or they lose their URLs (report). A translated home page's `footer` → that locale's entry in a locale map. Other footer content → drop (report). `heroImageDark` → Blume has no light/dark pair for a content image, so keep one (report).\n\n## Frontmatter\n\nBlume's schema is strict: every key below must be mapped or removed.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `metaTitle` | see \"Titles\" |\n| `description` | pass through |\n| `meta` (v1) / **v2** `head` | per entry, as in vitepress.md's frontmatter `head` row |\n| `canonicalUrl` (v1.7.1+) | `seo.canonical` |\n| `lang` | remove: Blume sets `<html lang>` per locale |\n| `permalink` | `slug`, the old path without its leading `/`, trailing `/`, or `.html` (see \"URLs and redirects\") |\n| `layout` naming a `.vuepress/components` or theme component | `mode: custom`, and rebuild the content, or use a custom `.astro` page (report). `layout: Layout` → remove |\n| `home` and the hero keys | see \"Home page\" |\n| `sidebar: auto` / **v2** `sidebar: heading` | remove: the outline rail lists the headings |\n| `sidebar: false` | `mode: center` |\n| **v2** `sidebar` as an array (a per-page sidebar) | remove (report) |\n| `sidebarDepth` | remove |\n| `prev` / `next` | same as VitePress |\n| `search: false` (v1) | remove (report): it hid the search box on that page, which stayed searchable |\n| `tags` (v1, extra search terms) | `search.keywords` |\n| **v2** `date` | keep on `type: blog` pages, otherwise remove |\n| **v2** `redirectFrom` / `redirectTo` | `redirects` (see the redirect plugin row) |\n| `navbar`, `pageClass`, `editLink`, **v2** `editLinkPattern`, `lastUpdated`, `contributors`, `externalLinkIcon`, `permalinkPattern`, `routeMeta` | drop (report) |\n| keys a theme reads | see \"Themes\" |\n\n## Themes\n\n- **A theme that `extend`s `@vuepress/theme-default`** (v1's `@vuepress/theme-vue`, or a local `.vuepress/theme/index.js` with `extend`) → treat it as the default theme. Layouts it overrides (ads, banners) → drop (report).\n- **An ejected or local theme** (a full `.vuepress/theme/` with `Layout.vue`, `Navbar.vue`, …) → map the `themeConfig` keys it shares with the default theme. Any extra key (a version list inside the sidebar, say) is custom, so read its components to see what it rendered (report).\n- **`vuepress-theme-hope`** (`hopeTheme({…})`, often in `.vuepress/theme.ts`):\n  - **Sidebar** `\"structure\"` builds the tree from files, which Blume does by default. `order` → `sidebar.order`, `index: false` → `sidebar.hidden: true`, and `shortTitle` → `sidebar.label`. Hope sorts positive `order`s first, then unordered pages by title, then negative `order`s (`-1` last), while Blume sorts a negative order first: renumber, or list the folder's order in its `meta.ts` `pages`. A folder `README.md`'s `dir` → that folder's `meta.ts`: `text` → `title`, `icon`, `order`, `collapsible` (on by default) → `display: \"group\"`, and `expanded: true` → `collapsed: false`. `dir.index: false` (the folder left out of the generated sidebar) → `sidebar.hidden: true` on each of its pages.\n  - **Icons** (frontmatter, navbar, sidebar) are FontAwesome or Iconify names → Lucide (SKILL.md \"Icons are Lucide\").\n  - **Markdown** (the theme's `markdown` options, or `plugins.mdEnhance`): `^sup^`, `~sub~`, task lists, and footnotes work as written. `==mark==` → `<mark>…</mark>`. `::: center` / `::: right` → drop the alignment. `!!spoiler!!` → drop. `<VPCard>` → `<Card>`. A ` ```component Name ` fence (YAML or JSON props) → that component with the props as attributes. `<Catalog />` → `directory: \"card\"` in that folder's `meta.ts`. `<!-- more -->` → delete. Charts (Chart.js, ECharts, Markmap, PlantUML, flowchart), playgrounds, `::: demo`, and slides → no equivalent: a mermaid diagram, an image, or a link (report).\n  - **Frontmatter:** `author` and `date` → `authors` and `date` on `type: blog` pages, otherwise drop. `category` and `tag` → drop (report: Blume has no tag pages), or `search.keywords`. `article`, `star`, `sticky`, `cover`, `isOriginal`, `pageInfo`, `breadcrumb`, `toc`, `comment`, `copy`, `footer`, `copyright`, `containerClass`, and any other hope-only key (`license`, `pageview`, `backToTop`, `sitemap`, `feed`, …) → drop. The project home's `highlights` → more cards. `bgImage` and the hero style keys → drop (report).\n\n## Icons\n\nThe default theme has no page icons. Icons you add to tabs, `meta.ts`, cards, or `sidebar.icon` are kebab-case Lucide names. Hope's icons map as above.\n\n## Assets\n\n- **`.vuepress/public/`** → `public/`. Root-absolute references (`/hero.png`) keep working.\n- **Relative images** (`./img.png`) work as written.\n- **Webpack paths** (v1 `~@alias/img.png`, `~some-package/img.png`, the `@source` alias) → copy the file into the content root or `public/` and link it plainly.\n\n## i18n\n\nMap it as in vitepress.md § \"i18n\". VuePress's `locales` keys are paths (`'/zh/'`), so the `code` is the folder (`zh`), not the `lang` (`zh-CN`). The default locale's `lang` (`en-US`) gives `defaultLocale` (`en`). The theme's per-locale options (v1 `themeConfig.locales`, **v2** the theme's `locales`) carry the rest:\n\n- `label` (**v2** `selectLanguageName`) → that locale's `label`. `selectText` (**v2** `selectLanguageText`), `ariaLabel`, `editLinkText`, the `lastUpdated` text, `serviceWorker`, and the other UI labels → drop, or `i18n.ui`.\n- `nav` → per-locale label maps on `navigation.tabs` / `actions`.\n- `sidebar` (keyed `'/zh/guide/'`, …) → each locale's own folder `meta.ts` files.\n- A per-locale container title (`defaultTitle: { '/zh/': '提示' }`) → drop: Blume's UI packs (including `zh`) title callouts.\n- A per-locale `title` / `description` → drop; a translated home page can keep its description in frontmatter.\n- **Anchors:** keep each locale's own old ids (`/zh/` pages had Chinese slugs, which zh pages and outside zh links use). The anchor script does this, pairing every page with its own old page. vitepress.md's \"Pin translated headings\" means exactly that, not pinning to the default locale's ids.\n\n## URLs and redirects\n\nFollow vitepress.md § \"URLs and redirects\", treating VuePress like `cleanUrls: false`: one exact redirect per non-index `.html` page, never a redirect from an index URL, both spellings for a moved route, and rewritten internal links. VuePress specifics:\n\n- **Build the old site first and keep its output** (`<source>/.vuepress/dist`, or `dest`): it's the URL list and the record of the old anchors. VuePress writes no sitemap unless a plugin adds one. v1 runs on webpack 4, which fails on current Node with `error:0308010C:digital envelope routines::unsupported`: build it with `NODE_OPTIONS=--openssl-legacy-provider`, or under Node 16 (`npx -p node@16 …`).\n- **Redirects:** run this in that output directory and paste the result into `redirects` (the VitePress codemod's `--redirects` reads only VitePress builds):\n\n  ```bash\n  find . -name '*.html' ! -name index.html ! -name 404.html | sed 's#^\\.##' | sort \\\n    | awk '{ to = $0; sub(/\\.html$/, \"\", to); printf \"  { from: \\\"%s\\\", to: \\\"%s\\\" },\\n\", $0, to }'\n  ```\n\n- **`README.md` → `index.md`** keeps the folder URL (`/guide/` is served from `guide/index.html`), so it needs no redirect. Rewrite each link to a `README.md` to its folder (`./README.md` → `./`, `/guide/README.md` → `/guide`). Once the file is renamed, `blume validate` flags the ones you miss.\n- **`permalink` pages** served the permalink, not the file path. Set `slug` to it. A permalink ending in `.html` also needs a redirect from that `.html` URL.\n- **Links:** VuePress authors link to `.md` files, relative or absolute (`/guide/foo.md`; **v2** recommends relative), and to `.html`. Rewrite them as vitepress.md says. Also fix links in `nav`, `sidebar`, `actionLink`, hero `actions`, and raw `<a href>`.\n- **Anchors: pin them with the bundled script.** VuePress slugs headings as VitePress does, so ids change on many pages (115 headings on 32 of 92 pages in VuePress's own docs, counted before Blume left badges and trailing emoji out of its ids). **Duplicate headings number differently:** VuePress's second `## Options` is `#options-2`, Blume's is `#options-1`. After `blume build`, run from the Blume project:\n\n  ```bash\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist>          # dry run\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist> --write  # apply\n  ```\n\n  It pairs each page's headings with the old build's by text and position, translated pages with their own old pages, and appends `[#old-id]` (`{#old-id}` in `.md`) where the ids differ. Rebuild and rerun until it reports 0. A `NOTE` names a heading whose source line it couldn't match, such as one with inline code, a Markdown escape (`\\<`), or `--` (19 of the 115 in VuePress's docs): pin those by hand. It also pins the home hero's `#main-title`, which is harmless. On a large site, `--only-linked` pins only the ids something links to.\n\n- **`validate` also reports anchors that were already broken.** VuePress never checked fragments (a zh page linking an English id, a renamed section). Point each at the right heading or drop the fragment, and report it.\n- **Hosts:** Netlify (with pretty URLs) and GitHub Pages also answered `/guide/foo` without `.html`, which Blume keeps. On Netlify, a static build for no named host writes unforced `_redirects` rules, so Netlify serves the meta-refresh page instead of a 301: use `deployment: netlify({ output: \"static\" })` (see Redirects in `docs/02-deployment.mdx`). The GitHub Pages caveat in vitepress.md applies.\n\n## Teardown\n\n- Remove `vuepress`, `vuepress-vite` / `vuepress-webpack`, `@vuepress/*`, `vuepress-plugin-*`, `vuepress-theme-*`, `vue`, `vue-template-compiler`, `vue-server-renderer`, and any markdown-it, Stylus, Sass, or webpack packages that only served VuePress. Keep `vue` with `@astrojs/vue` if you made Vue islands.\n- Delete `.vuepress/` (config, `config/`, `components/`, `theme/`, `styles/`, `enhanceApp.js` / `client.*`, `dist/`, `.temp/`, `.cache/`) once `public/` has moved.\n- Repoint the scripts (`docs:dev`, `docs:build`) to `blume dev` / `blume build` / `blume preview`, and add `blume`. Remove `NODE_OPTIONS=--openssl-legacy-provider`.\n- **Node:** Blume needs 22.19 or later, and v1 sites often pin an old one (`.nvmrc`, `engines`, CI `node-version: '14'`, Netlify's `NODE_VERSION`). Bump each. When the same CI job also builds or tests other packages (yarn 1 refuses to install a dependency whose `engines` excludes the running Node), the bump reaches them too, so report it.\n- Grep the repo for `.vuepress/dist`, `dest`, and `vuepress build`. Deploy scripts (the VuePress guide's `deploy.sh` pushing `docs/.vuepress/dist` to `gh-pages`), `netlify.toml` publish dirs, and CI workflows must now publish Blume's `dist/`. Settings kept in a host's dashboard (Netlify's publish directory, build command, and `NODE_VERSION`) can't be committed: list them for the user, with a full (not shallow) clone if you set `lastModified: \"git\"`.\n- `.gitignore`: replace the docs' own entries (`.vuepress/dist`, the old `dest`) with `.blume` and `dist`. Keep entries other packages in the repo still use (`.temp`, `.cache`).\n- A `404.md` → delete it (as in VitePress).\n\n## Dropped — report these\n\nEverything vitepress.md's list covers that this site used, plus: v1 Markdown slots, `@flowstart` diagrams that weren't redrawn, the PWA and service worker, `sidebarDepth` and heading-only (`auto`) sidebars, `nextLinks` / `prevLinks` when only one was off, v1 `<code-block active>` defaults that couldn't move, Vue 2 components that weren't ported, `enhanceApp` / client-config code, `.vue` pages, `pageClass` and layout components, Stylus/Sass palette overrides, Universal Analytics ids, the DocSearch index, GitLab and Bitbucket edit links, old-site anchors that were already broken, hope-only components and frontmatter, and any `note` / `info` text that v1 showed literally and now renders as a callout.\n",".agents/skills/blume-update-docs/SKILL.md":"---\nname: blume-update-docs\ndescription: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.\n---\n\n# Update Blume Docs\n\nBlume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, `blume build` validates frontmatter and duplicate routes, and `blume validate` checks links and anchors.\n\nYour job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.\n\n## Ground rules\n\n- **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.\n- **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.\n- **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.\n- **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.\n- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.\n\n## Workflow\n\n1. **Establish context.**\n   - Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.\n   - Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.\n   - If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.\n\n2. **Reuse or create a maintenance branch.**\n   - If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.\n   - Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.\n\n3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:\n   - Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.\n   - Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.\n   - Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.\n   - Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.\n\n4. **Update the docs.**\n   - Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.\n   - Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.\n\n5. **Verify.**\n   - Run the docs build (`blume build` or the repo's documented docs QA) — it validates frontmatter and duplicate routes.\n   - Run `blume validate` to check internal links and anchors.\n   - Run lint/format/typecheck when the repo's conventions call for them on docs changes.\n   - Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.\n\n6. **Deliver.**\n   - **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.\n   - **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.\n\n## Resources\n\n- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.\n",".agents/skills/blume-update-docs/references/audit-checklist.md":"# Audit checklist\n\nUse this checklist to decide whether a docs-maintenance run should make changes.\n\n## Sources to check\n\n- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.\n- **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.\n- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.\n- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.\n- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.\n- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.\n- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.\n- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.\n\n## Change criteria\n\nMake a docs edit when at least one condition is true:\n\n- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.\n- A documented workflow no longer works or misses a required step.\n- A page promises a feature, provider, adapter, or integration the code no longer supports.\n- A new user-facing capability shipped but is absent from the appropriate docs page.\n- A link points at moved, removed, or outdated primary documentation.\n- A changelog or release page needs an entry for shipped user-facing behavior.\n\nSkip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.\n\n## Blume editing guidance\n\n- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.\n- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.\n- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.\n- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.\n- Keep internal links root-relative (`/guides/setup`), in the form nearby pages already use.\n- Do not edit generated `.blume/` or `dist/` output.\n\n## PR notes\n\nInclude these sections in the PR body or no-op summary:\n\n- Sources checked\n- Docs changed\n- Verification run (commands and results)\n- Skipped checks, with reasons\n- Remaining risk or follow-up\n",".agents/skills/blume/SKILL.md":"---\nname: blume\ndescription: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.\n---\n\n# Blume\n\nBlume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.\n\nThe core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.\n\n## What makes it different\n\n- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.\n- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional in-page **assistant** or an **MCP server** so coding agents read your docs directly.\n- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.\n- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.\n\n## Quickstart\n\nBlume needs **Node.js 22.19 or newer**. From an empty folder:\n\n```bash\nnpx blume init   # scaffold docs/index.mdx, blume.config.ts, and package.json scripts, then install\nnpm run dev      # dev server with hot reload\nnpm run build    # static HTML to dist/, with a local search index\n```\n\nIn a project that already has a `package.json`, `blume init` leaves it alone: add `\"dev\": \"blume dev\"` and `\"build\": \"blume build\"` to its scripts, or run `npx blume dev`. Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.\n\n### Writing a page\n\nEvery page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.\n\n```mdx\n---\ntitle: Introduction\ndescription: Welcome to my docs.\n---\n\nWelcome! Use **Markdown** and built-in components — no imports required:\n\n:::note\nBlume ships callouts, cards, tabs, steps, and more.\n:::\n```\n\nNavigation, search, and page metadata are inferred from your files as you add them.\n\n## Upgrading from Blume 1\n\nBlume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the assistant's model backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), Ask AI is renamed the assistant (`ai.ask` becomes `ai.assistant`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:\n\n```bash\nnpx blume@latest upgrade\n```\n\nIt bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--codex` or `--claude` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.\n\n## Migrating from another framework\n\nTo move a Mintlify, Docusaurus, GitBook, MkDocs, ReadMe, or other docs site to Blume, the user runs `npx blume migrate [source] --codex` (or `--claude`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.\n\nEvery build publishes a generated agent skill for the site at `/skill.md` (`agents.skillMd`). To write a richer one from the docs, the user runs `blume skill --codex` (or `--claude`), which opens an agent on the `blume-write-skill` skill. When you are that agent, or the user asks you to write the site's skill directly, follow `skills/blume-write-skill/SKILL.md` in the installed package.\n\n## What's included\n\n- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.\n- **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).\n- **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.\n- **Narration** — a \"Listen to this page\" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build; `openai({ model, voice })` uses OpenAI's speech models instead, and with a `baseUrl` a self-hosted OpenAI-compatible speech server, and needs `@ai-sdk/openai` installed).\n- **Navigation** — inferred from files, refined with `meta.ts` or config.\n- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.\n- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.\n- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.\n\n## How it works\n\nThe Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.\n\n## Full documentation\n\nThis is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the `docs/` directory bundled inside the installed `blume` package.\n\n**Locate the package first — it is not always at the repository root.** In a workspace monorepo (pnpm especially), `blume` is installed in the depending workspace's `node_modules` (e.g. `apps/docs/node_modules/blume/docs`), not the root. From the package that depends on `blume`, this prints the exact location:\n\n```bash\nnode -e \"console.log(require.resolve('blume/package.json'))\"\n```\n\nThe docs sit in `docs/` next to that `package.json`. Start with `docs/index.mdx` (Introduction) and `docs/01-quickstart.mdx`, then list the `docs/` directory: each section is a folder — configuration, content authoring, API references, discoverability (SEO and the agent-facing surface), the CLI, and advanced topics — so open the one that covers the task.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository\n\noRPC — typesafe API framework. pnpm monorepo (Node 22+, `pnpm` workspaces) with packages in `packages/*`, docs/website in `apps/content` (Blume), example apps in `playgrounds/*`, root-level e2e tests in `tests/`, and benchmarks in `benches/`.\n\n## Commands\n\n```bash\npnpm install                 # setup\npnpm test                    # all tests: per-package tests + root vitest\npnpm vitest run <path>       # single test file (from repo root), e.g. pnpm vitest run packages/server/src/builder.test.ts\npnpm type:check              # tsc across all packages — this is also what runs the *.test-d.ts type tests\npnpm lint                    # eslint (also the formatter — @antfu/eslint-config)\npnpm lint:fix\npnpm bench                   # vitest bench\npnpm --filter @orpc/server prepack # build one package (unbuild, runs automatically on pack/publish); builds are NOT needed for dev/tests\n```\n\n- Root vitest config (`vitest.config.ts`) runs all `*.test.ts` with `globals: true`, plus a jsdom project for `*.test.tsx` in `packages/next`, `packages/tanstack-query`, `packages/pinia-colada` and `packages/swr`.\n- Exceptions: `packages/bun` runs with `bun test`, `packages/cloudflare` with its own vitest (workerd) — both are excluded from the root vitest and root tsconfig. Run their tests via `pnpm --filter <pkg> test`. `packages/nest` is excluded only from the root tsconfig (type-check it with `pnpm --filter @orpc/nest type:check`); its tests run in the root vitest.\n- Docs site (Blume): `cd apps/content && pnpm dev` (`blume dev`); `pnpm build` runs `blume build`. Content lives in `.mdx` files; custom pages in `apps/content/pages/`. The dev server caches content-collection frontmatter for custom pages — restart it after editing blog-post frontmatter. `pnpm docs:validate` (root) runs the JSDoc backlink checker plus `blume validate --strict` (links, anchors, assets); CI runs it too.\n- PR titles follow Conventional Commits. Scope rules:\n  - Use the package name (e.g. `feat(server): ...`), or `rpc`/`openapi` for changes tied to a protocol rather than a single package, such as its serializers, handlers, links, or dedicated plugins.\n  - Use `content` only for the docs site itself (styles, Blume config, ...), and only with the `chore` type — never `feat`/`fix`/`perf`/`docs`, since these changes do not affect library users. Documentation about a package or protocol uses that package/protocol scope instead (e.g. `docs(openapi): ...`).\n  - Omit the scope when the change is too broad for a single one.\n","packages/ai-sdk/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ai-sdk\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fai-sdk?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/arktype/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/arktype\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Farktype?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/bun/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/bun\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fbun?logo=npm\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/client/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/client\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fclient?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/cloudflare/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/cloudflare\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcloudflare?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/contract/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/contract\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcontract?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/effect/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-effect\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-effect?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/evlog/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/evlog\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fevlog?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/hibernation/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/hibernation\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fhibernation?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/json-schema/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/json-schema\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fjson-schema?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/lock/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-lock\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-lock?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/mcp/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/mcp\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmcp?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/msw/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/msw\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmsw?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/nest/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/nest\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnest?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/next/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/next\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnext?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/node/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/node\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnode?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/openapi/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/openapi\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopenapi?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/opentelemetry/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/opentelemetry\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopentelemetry?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/pinia-colada/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pinia-colada\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpinia-colada?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/pino/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pino\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpino?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/publisher/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/publisher\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpublisher?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/ratelimit/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ratelimit\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fratelimit?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/server/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/server\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fserver?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/shared/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fshared%2FREADME.md -->","packages/swr/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fswr%2FREADME.md -->","packages/tanstack-query/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftanstack-query%2FREADME.md -->","packages/trpc/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftrpc%2FREADME.md -->","packages/valibot/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fvalibot%2FREADME.md -->","packages/zod/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fzod%2FREADME.md -->","skills/orpc-contract/SKILL.md":"---\nname: orpc-contract\ndescription: \"Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill.\"\nlicense: MIT\n---\n\n# oRPC Contract-First\n\nContract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with `oc` from `@orpc/contract`, and an implementation built from it with `implement` from `@orpc/server`. Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the `os`-first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with `unlazyRouter` first).\n\nThis skill targets oRPC v2. The `orpc` skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see [Full documentation](#full-documentation)).\n\n## Define the contract with oc\n\nEvery chain is optional and each call returns a new instance, so share base contracts freely. A contract has no `.handler`. Zod, Valibot, ArkType, and any other Standard Schema library work.\n\n```ts\nimport { oc } from '@orpc/contract'\nimport * as z from 'zod'\n\nexport const contract = {\n  planet: {\n    list: oc\n      .output(z.array(z.object({ id: z.number(), name: z.string() }))),\n    find: oc\n      .errors({ NOT_FOUND: {} })\n      .input(z.object({ id: z.number() }))\n      .output(z.object({ id: z.number(), name: z.string() })),\n  },\n}\n```\n\n- Always define `.output`: without it clients infer the output as `unknown`.\n- A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n- Attach shared metadata to a whole subtree with `oc.meta(someMeta).router({...})`.\n- `.errors({ NOT_FOUND: {} })` declares typesafe errors; implementations throw them via `errors.NOT_FOUND()` and clients infer their shapes.\n- Repeated `.input`/`.output` calls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields.\n- Schema-library-free contracts use the `type` utility from `@orpc/contract`: `oc.input(type<{ value: number }>())`, optionally with a mapping function as `type<Input, Output>(fn)`.\n- REST routes attach via `.meta(openapi({ method: 'GET', path: '/planets/{id}' }))` from `@orpc/openapi`, exactly as on `os`; routing rules, `prefix`, and spec generation belong to the `orpc-openapi` skill.\n\nInfer types with `InferRouterContractInputs`, `InferRouterContractOutputs`, and `InferRouterContractErrors` from `@orpc/contract`.\n\n## Implement with implement\n\n`implement` turns the contract into an implementer that mirrors its shape and type-checks every handler; `.router` also enforces the contract at runtime.\n\n```ts\nimport { implement } from '@orpc/server'\n\nconst implementer = implement(contract).$context<{ db: DB }>()\n\nconst listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())\n\nconst findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {\n  const planet = await context.db.find(input.id)\n  if (!planet)\n    throw errors.NOT_FOUND()\n  return planet\n})\n\nexport const router = implementer.router({\n  planet: { list: listPlanets, find: findPlanet },\n})\n```\n\n- `.$context` declares the initial context the procedures require, as on `os`.\n- Apply middleware per procedure with `.use(mw)` before `.handler`. That runs after input validation (the contract already registered `.input`); to wrap validation, apply it at router level: `implementer.use(mw)` for every procedure, or `implementer.planet.use(mw).list` for a subtree. Router-level plus procedure-level `.use` can run the same middleware twice; use the dedupe pattern from the `orpc` skill.\n- `implementer.middleware(fn)` creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the `in` operator: `if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS()`. Any type-compatible middleware also works.\n- The result is a normal router: serve it with `RPCHandler` (`orpc` skill) or `OpenAPIHandler` (`orpc-openapi` skill), call it in-process with `call` or `createRouterClient` from `@orpc/server`.\n\n## Consume the contract from clients\n\n`RPCLink` needs only the contract type; `OpenAPILink` takes the contract as a runtime value to read each procedure's route. Get the client types exactly right:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\n// RPC protocol (server side is RPCHandler)\nconst rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })\nconst client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)\n\n// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)\nconst openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })\nconst apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)\n```\n\n- Router-first equivalents use `RouterClient<typeof router>` from `@orpc/server` in the same positions.\n- `JsonifiedClient` is required over `OpenAPILink` because OpenAPI serialization is one-way (a `Date` returns as a string); dropping it via Smart Coercion, plus `OpenAPILink` options and CORS caveats, are in the `orpc-openapi` skill.\n- Per-call client context is the second type parameter, `RouterContractClient<typeof contract, ClientContext>`, then `client.planet.find(input, { context: { token } })`; link options like `headers` accept functions of that context.\n- Export `RouterContractClient<typeof contract>` as a type from the server package so clients never import the contract module itself (still needed as a runtime value for `OpenAPILink`; ship the minified JSON below).\n- In very large codebases, skip the root client: pin each procedure with `.meta(meta.path([...]))` and build per-procedure clients with `createContractClientFactory` from `@orpc/contract` (`createContractJsonifiedClientFactory` from `@orpc/openapi` when the link needs `JsonifiedClient`); fetch `contract/client-factory` before adopting it.\n\n## Ship the contract\n\nWhen the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:\n\n```ts\nimport fs from 'node:fs'\nimport { minifyRouterContract } from '@orpc/contract'\nimport { unlazyRouter } from '@orpc/server'\n\nconst minified = minifyRouterContract(await unlazyRouter(router))\nfs.writeFileSync('./contract.json', JSON.stringify(minified))\n```\n\n`minifyRouterContract` keeps only client-needed metadata. On the client, import the JSON and cast, since schemas do not survive serialization: `new OpenAPILink(contract as typeof router, ...)`.\n\nTo publish a typed SDK to npm, export a factory that pairs the contract with a link:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nexport function createMyApi(apiKey: string): RouterContractClient<typeof contract> {\n  const link = new RPCLink({\n    origin: 'https://example.com',\n    url: '/rpc',\n    headers: { 'x-api-key': apiKey },\n  })\n  return createORPCClient(link)\n}\n```\n\nBundle with `tsdown --dts src/index.ts`, point `exports` at `dist` types plus import entries, list `@orpc/client` and `@orpc/contract` as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch `recipes/publish-client-to-npm` for the complete `package.json`.\n\n## Generate the contract from an existing OpenAPI spec\n\nUse Hey API's `orpc` plugin instead of hand-writing the contract. Install `@hey-api/openapi-ts@next` as a dev dependency (oRPC v2 output requires the `next` tag until the next stable Hey API release) and create `openapi-ts.config.ts`:\n\n```ts\nimport { defineConfig } from '@hey-api/openapi-ts'\n\nexport default defineConfig({\n  input: 'https://example.com/openapi.json', // local file or URL\n  output: 'src/contract',\n  plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],\n})\n```\n\nThen run `npx @hey-api/openapi-ts`. It writes `orpc.gen.ts` (one procedure contract per operation, routed via `.meta(openapi({...}))` with `inputStructure: 'detailed'`, plus a combined `contract` router) and `zod.gen.ts`. The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install those too. From there, implement the contract on your own server, or point `OpenAPILink` at the existing spec-compliant server.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/contract/procedure.md)\n\nPages to fetch when you need details beyond this skill:\n\n- Contract: `contract/procedure`, `contract/router`, `contract/implementation`, `contract/generate-from-openapi`\n- Clients: `client/client-side`, `client/server-side`, `client/error-handling`, `openapi/link`\n- Workflows: `recipes/publish-client-to-npm`, `contract/client-factory`, `recipes/monorepo-setup`\n","skills/orpc-migrate/SKILL.md":"---\nname: orpc-migrate\ndescription: \"Migrate existing codebases to current oRPC, covering tRPC to oRPC (incremental wrapping via the @orpc/trpc integration or a full rewrite with the concept mapping) and oRPC v1 to v2 (package renames, breaking changes, and a safe order of operations). Use when asked to migrate from tRPC to oRPC, convert or wrap a tRPC router, upgrade oRPC v1 to v2, fix oRPC v2 breaking changes, or swap `@trpc/*` packages for `@orpc/*` equivalents. Biases toward retrieval from the oRPC docs over pre-trained knowledge. Not for greenfield oRPC work or new features in an already-migrated codebase: use the orpc skill for those.\"\nlicense: MIT\n---\n\n# Migrating to oRPC\n\nPlaybook for two migrations: tRPC to oRPC, and oRPC v1 to v2. Work in small mechanical steps and run the project's typecheck and test suite after each one, so any failure points at the last step. Pretrained knowledge of oRPC describes v1 and is often wrong for v2: derive every import path, builder method, and option name from the docs pages listed at the end, never from memory. For core v2 concepts while rewriting (the `os` builder, routers, middleware, clients), load the `orpc` skill. v2 currently ships under the `beta` npm dist-tag and plain installs get v1; drop the `@beta` suffix once `npm view @orpc/server dist-tags` shows `latest` at 2.x.\n\n## tRPC to oRPC\n\nTwo paths. Pick incremental when the app must keep shipping or the tRPC router is large; pick the full rewrite when the router is small enough to convert in one pass.\n\n### Incremental: wrap the existing tRPC router\n\nInstall `@orpc/trpc@beta` and convert. The result is a regular oRPC router: expose it through an RPC or OpenAPI handler, or call it with a server-side client, while the tRPC code keeps working untouched.\n\n```ts\nimport { toORPCRouter } from '@orpc/trpc'\n\nconst orpcRouter = toORPCRouter(trpcRouter)\n```\n\n- tRPC error formatting is not supported: tRPC errors arrive wrapped in `ORPCError` with the `TRPCError` as `cause` (and a `ZodError` below that for validation failures). Reshape them in a handler interceptor if consumers need structured errors.\n- `toTRPCMeta` bridges oRPC `openapi()` metadata into tRPC `.meta()` so converted procedures get OpenAPI routing. Chained tRPC `.meta()` calls merge shallowly, so keep all oRPC metadata inside a single `toTRPCMeta` call.\n\nThen rewrite leaf routers to native oRPC one at a time, mounting each next to the converted router in one plain object.\n\n### Full rewrite: concept map\n\n| Concept           | tRPC                                           | oRPC                           |\n| ----------------- | ---------------------------------------------- | ------------------------------ |\n| Router            | `t.router({...})`                              | plain object                   |\n| Procedure builder | `t.procedure`                                  | `os`                           |\n| Context           | `initTRPC.context<T>()`                        | `os.$context<T>()`             |\n| Create middleware | `t.middleware(fn)`                             | `os.middleware(fn)`            |\n| Use middleware    | `.use(mw)`                                     | `.use(mw)`                     |\n| Validation        | `.input(schema)` / `.output(schema)`           | same names                     |\n| Implementation    | `.query()` / `.mutation()` / `.subscription()` | `.handler()` for all three     |\n| Errors            | `new TRPCError({ code, ... })`                 | `new ORPCError(code, { ... })` |\n| Serializer        | `superjson` transformer                        | built in, remove `superjson`   |\n\nSteps, in order, verifying after each:\n\n1. **Packages.** Remove `@trpc/server`, `@trpc/client`, `@trpc/tanstack-react-query`; install `@orpc/server@beta`, `@orpc/client@beta`, `@orpc/tanstack-query@beta`.\n2. **Base file.** Port the context factory unchanged, then rebuild the shared procedures. In handlers and middleware, `ctx` becomes `context`:\n\n   ```ts\n   import { ORPCError, os } from '@orpc/server'\n\n   const o = os.$context<Awaited<ReturnType<typeof createContext>>>()\n\n   export const publicProcedure = o.use(timingMiddleware)\n   export const protectedProcedure = publicProcedure.use(({ context, next }) => {\n     if (!context.session?.user)\n       throw new ORPCError('UNAUTHORIZED')\n     return next({ context: { session: context.session } })\n   })\n   ```\n\n3. **Procedures.** Replace `.query`/`.mutation`/`.subscription` with `.handler`; `.input` and `.output` carry over as is.\n4. **App router.** Delete every `createTRPCRouter()` wrapper; nested plain objects are the router.\n5. **Server.** Replace the tRPC adapter with an oRPC handler for the runtime (fetch shown; other adapters exist for Node, Fastify, AWS Lambda, WebSocket):\n\n   ```ts\n   import { RPCHandler } from '@orpc/server/fetch'\n\n   const handler = new RPCHandler(appRouter)\n\n   const { response } = await handler.handle(request, {\n     prefix: '/api/orpc',\n     context: await createContext({ headers: request.headers }),\n   })\n   ```\n\n6. **Client.** `RPCLink` plus `createORPCClient`, typed by `RouterClient`. Call sites drop the `.query()`/`.mutate()` suffixes:\n\n   ```ts\n   import type { RouterClient } from '@orpc/server'\n   import { createORPCClient } from '@orpc/client'\n   import { RPCLink } from '@orpc/client/fetch'\n\n   const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })\n   export const client: RouterClient<typeof appRouter> = createORPCClient(link)\n\n   const { planets } = await client.planet.list({ cursor: 0 })\n   ```\n\n7. **TanStack Query.** `createTanstackQueryUtils(client)` replaces the provider and `useTRPC` hook entirely; use the utils object directly. Input moves inside an `input` key: `orpc.planet.list.queryOptions({ input: { cursor: 0 } })`, `orpc.planet.create.mutationOptions()`. For infinite queries, `infiniteOptions` takes `input` as a function of the page param.\n\n## oRPC v1 to v2\n\nMost v1 names still compile through deprecated aliases (strike-through hints, not errors), so migrate in passes. Order of operations:\n\n1. **Update packages.** Install every `@orpc/*` package from the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`, and so on). Swap renamed ones first: `@orpc/react-query`/`@orpc/vue-query`/`@orpc/solid-query`/`@orpc/svelte-query` all became `@orpc/tanstack-query`; `@orpc/openapi-client` merged into `@orpc/openapi`; `@orpc/react` became `@orpc/next`; `@orpc/otel` became `@orpc/opentelemetry`; the `experimental-` packages were promoted (`@orpc/publisher`, `@orpc/ratelimit`, `@orpc/pino`, `@orpc/swr`); `@orpc/vue-colada` became `@orpc/pinia-colada`. Typecheck: the remaining errors are the hard breaks.\n2. **Fix the hard breaks** (no aliases):\n   - Routing: `.route`, `.prefix`, `.tag`, `.$route` are gone from the builder. Use `.meta(openapi({ method, path, prefix, tags }))` from `@orpc/openapi`, or restore `.route` with `import '@orpc/openapi/extensions/route'`.\n   - `.callable` and `.actionable`: use `call`/`createRouterClient` from `@orpc/server` and `createServerFunctionable` from `@orpc/next`, or the corresponding extension imports.\n   - `RPCLink`: the single `url` split into `origin` plus a path-only `url`.\n   - Errors: `status` was removed from `ORPCError` and `.errors` definitions; map codes to HTTP status with `errorStatusMap` on the handler.\n   - `safe()`: the third tuple element is now the typed error itself (or `null`) and a fourth `isSuccess` element was added.\n   - Option renames, scoped: handler `rootInterceptors` to `routingInterceptors` (handler `clientInterceptors` still exists, unchanged); link `clientInterceptors` to `transportInterceptors`. Flat `eventIterator*` options moved under the adapter's request/response mapping: `toFetchResponse.eventStream` on the fetch handler, `sendStandardResponse.eventStream` on Node, `toFetchRequest.eventStream` on the link.\n   - `adapterInterceptors` was removed from handlers and links, because regular interceptors can now customize body parsing behavior.\n3. **Audit silent behavior changes** (compile fine, behave differently):\n   - **Wire format changed:** a v1 link cannot talk to a v2 server, in either direction. Deploy the upgraded server and clients together.\n   - **Automatic middleware deduplication removed:** middleware applied at both router and procedure level now runs twice, with no warning. Guard shared middleware with the context-flag pattern from the dedupe-middleware recipe.\n   - **Batch Plugin `exclude` became `filter` with the opposite meaning.** Usually delete `exclude`; if skipping is still needed, negate the predicate.\n   - **`RPCHandler` rejects GET by default** (`allowMethods` defaults to POST/PUT/PATCH/DELETE). Simplest fix: stop sending GET from the link; only allow GET deliberately, with CSRF protection.\n   - **Handler `filter` takes positional arguments now;** the v1 destructured form still type-checks but reads wrong values.\n   - **`.input`/`.output` now stack:** a repeated call adds a schema instead of replacing the previous one.\n4. **Sweep deprecated aliases** last: `eventIterator` to `asyncIteratorObject`, handler plugins gained a `HandlerPlugin` suffix and link plugins a `LinkPlugin` suffix, `ContractRouter*` types became `RouterContract*`. The from-v1 guide ends with the full alias cheat sheet.\n\nVerification: typecheck and unit tests after steps 1, 2, and 4; step 3 needs integration or e2e tests, since those changes never surface at compile time. Before finishing, grep for old package names and remaining deprecation strike-throughs.\n\n## Docs retrieval\n\nFetch pages instead of recalling them, and if this skill and a fetched page disagree, trust the page. The v2 docs live at https://orpc.dev and the v1 docs at https://v1.orpc.dev; slugs look alike across both hosts, so check which host a page came from before copying anything from it. The index of every v2 docs page is at https://orpc.dev/llms.txt, https://orpc.dev/llms-full.txt bundles the entire docs in one large file, and appending `.md` to any page URL returns its exact source markdown.\n\nAuthoritative pages to consult during the migration (this skill deliberately omits their full mapping tables):\n\n- https://orpc.dev/docs/migrations/from-trpc : side-by-side tRPC/oRPC code for every step, including server setup per framework\n- https://orpc.dev/docs/migrations/from-v1 : every v2 breaking change with v1/v2 comparisons, package rename table, and the deprecated alias cheat sheet\n- https://orpc.dev/docs/integrations/trpc : `toORPCRouter` and `toTRPCMeta` reference for the incremental path\n","skills/orpc-openapi/SKILL.md":"---\nname: orpc-openapi\ndescription: \"Expose an oRPC router as a spec-compliant OpenAPI HTTP API. Use when a project depends on @orpc/openapi, or for defining REST-style routes on oRPC procedures (openapi() metadata or .route with method, path, successStatus), serving them with OpenAPIHandler alongside RPCHandler, coercing query and path strings with Smart Coercion, calling an OpenAPI-shaped API with OpenAPILink, generating an OpenAPI 3.2 (or 3.1, 3.0) document with OpenAPIGenerator, or serving Scalar or Swagger docs with the OpenAPI Reference plugin. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For contract-first work (defining contracts with oc, implementing them with implement, or generating a contract from an existing OpenAPI spec), use the orpc-contract skill; for plain RPC serving, core builder, middleware, or client work with no REST exposure, use the orpc skill instead.\"\nlicense: MIT\n---\n\n# oRPC over OpenAPI\n\noRPC procedures speak two protocols from one router: the RPC protocol (`RPCHandler`/`RPCLink`) and plain OpenAPI HTTP (`OpenAPIHandler`/`OpenAPILink`). This skill covers the OpenAPI side. For core builder, middleware, context, and client concepts, load the `orpc` skill (it also carries the v2 install and version check); for contract-first workflows with `@orpc/contract`, load the `orpc-contract` skill.\n\nThis skill targets oRPC v2. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its exact docs page first (see [Full documentation](#full-documentation)).\n\n## Routing\n\nA procedure defaults to `POST` with a path derived from the router structure (`planet.create` becomes `POST /planet/create`). Override with `openapi` metadata, on server (`os`) and contract (`oc`) builders alike:\n\n```ts\nimport { openapi } from '@orpc/openapi'\nimport { os } from '@orpc/server'\nimport { z } from 'zod'\n\nconst getPlanet = os\n  .meta(openapi({ method: 'GET', path: '/planets/{id}', successStatus: 200 }))\n  .input(z.object({ id: z.string() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n- Path params: put `{id}` in `path` and the same key as a required field in the input schema. Use `{+path}` for catch-all segments that may contain `/`.\n- `prefix` prepends a path to a procedure or a whole router: `os.meta(openapi({ prefix: '/api/v2' })).router({...})`. Always set a `prefix` on lazy routers so lazy loading only triggers for relevant requests.\n- Merging across repeated `.meta(openapi(...))` calls: `prefix` and `tags` concatenate, `method`/`path`/`successStatus` last-wins, and setting a field to `undefined` resets it to the default.\n- `successStatus` defaults to `200` and must be below 400.\n\nFor direct routing without `.meta(openapi(...))`, enable the `.route` extension with a side-effect import in a module that always runs at startup (base builder file or server entry):\n\n```ts\nimport '@orpc/openapi/extensions/route'\n\nconst ping = os.route({ method: 'GET', path: '/ping' }).handler(async () => 'pong')\n```\n\n## Input and output mapping\n\nCompact mode (default): path params merge with query params or the request body depending on the HTTP method, so `GET /planets/earth?q=life` yields input `{ id: 'earth', q: 'life' }`. The handler's return value becomes the response body with status `successStatus`.\n\nSet `inputStructure: 'detailed'` in the metadata to receive `{ params, query, headers, body }` instead (define only the fields you need in the input schema). Set `outputStructure: 'detailed'` to return `{ status?, headers?, body? }` and vary the status per response.\n\nQuery strings and form data are decoded with bracket notation (fetch `openapi/bracket-notation` before designing schemas for nested query input; its limits are not guessable):\n\n- Repeated keys become arrays: `?color=red&color=blue` gives `['red', 'blue']`; `color[]=red` pushes too.\n- `[number]` targets an array index; `[key]` targets an object property: `?filter[status]=active` gives `{ filter: { status: 'active' } }`.\n- It cannot represent empty objects or arrays, root-level arrays, or objects whose keys are all numbers, and query/form values always arrive as strings (or files, in form data).\n\nOverride decoding per parameter with `paramsStyles` (`'primitive'`, `'comma-delimited-array'`, `'comma-delimited-object'`) and `queryStyles` (those plus `'array'`, `'json'`, and space/pipe-delimited variants). Use `requestBodyHint`/`responseBodyHint` (`'json'`, `'form-data'`, `'event-stream'`, `'octet-stream'`, `'file'`, ...) when headers alone cannot tell the parser how to handle the body, for example a raw `ReadableStream` upload.\n\n## Serving: OpenAPIHandler\n\nImport from the adapter subpath (`@orpc/openapi/fetch`, `@orpc/openapi/node`, ...):\n\n```ts\nimport { SmartCoercionHandlerPlugin } from '@orpc/json-schema'\nimport { OpenAPIHandler } from '@orpc/openapi/fetch'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new SmartCoercionHandlerPlugin({ converters: [new ZodToJsonSchemaConverter()] }),\n  ],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} })\n  return matched ? response : new Response('Not Found', { status: 404 })\n}\n```\n\n`OpenAPIHandler` coexists with `RPCHandler`: both accept the same router, so mount them on different prefixes (for example `/api` and `/rpc`) and try each in turn, returning the first `matched` response.\n\nSmart Coercion: query, path, and form values arrive as strings, so add `SmartCoercionHandlerPlugin` whenever input schemas expect non-string types from those sources. It coerces schema-driven, lossless conversions only (`'123'` to `123`, `'true'`/`'on'` to `true`, ISO strings to `Date`, arrays to `Set`/`Map` via `x-native-type`) and leaves ambiguous values untouched. Skip it when you already coerce in the schema or performance is critical; it adds runtime overhead.\n\nOther handler options: `interceptors`/`routingInterceptors`/`clientInterceptors` for logging and error mapping, `filter` to exclude procedures from matching, and `errorStatusMap` plus `customErrorResponseBodyEncoder` to customize error responses (by default `ORPCError` codes map to statuses via `COMMON_ERROR_STATUS_MAP`, for example `NOT_FOUND` to 404).\n\n## Calling: OpenAPILink\n\n`OpenAPILink` calls an OpenAPI-shaped oRPC API (or any spec-compliant server) through a typesafe client. It needs the contract or router type to know each procedure's route:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\nconst link = new OpenAPILink(contract, {\n  origin: 'https://api.example.com',\n  url: '/api',\n  headers: ({ context }) => ({\n    authorization: context?.token ? `Bearer ${context.token}` : undefined,\n  }),\n})\n\nconst client: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)\n```\n\nWith a router instead of a contract, type the client as `JsonifiedClient<RouterClient<typeof router>>` (`RouterClient` from `@orpc/server`). `JsonifiedClient` exists because OpenAPI serialization is one-way: a `Date` returns as a string. Add `SmartCoercionLinkPlugin` from `@orpc/json-schema` (same converters, first argument is the contract) to restore native types on responses, then drop the `JsonifiedClient` wrapper from the client type.\n\nTo ship a contract to clients without bundling server code, minify it to JSON and import it with a cast; the flow is in the `orpc-contract` skill under \"Ship the contract\".\n\n## Spec document and interactive docs\n\n`OpenAPIGenerator` turns a router or contract into an OpenAPI 3.2 document (pass any `3.1.x` or `3.0.x` `version` for tools that stop at an older version; `QUERY` procedures need 3.2). `OpenAPIReferenceHandlerPlugin` serves the spec at `/spec.json` and a Scalar UI at `/` under the handler prefix (change with `specPath`/`docsPath`, or set `provider: 'swagger'` for Swagger UI):\n\n```ts\nimport { OpenAPIGenerator } from '@orpc/openapi'\nimport { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] })\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new OpenAPIReferenceHandlerPlugin({\n      spec: () => generator.generate(router, {\n        base: {\n          info: { title: 'Planet API', version: '1.0.0' },\n          servers: [{ url: '/api' }], // absolute URL in production\n        },\n      }),\n    }),\n  ],\n})\n```\n\nEnrich the document through `openapi` metadata: `operationId`, `summary`, `description`, `tags`, `successDescription`, and a `spec` callback that receives the generated operation object and returns an extended one (security requirements, extra responses). Write `spec` and `base` as OpenAPI 3.2 objects even when generating 3.1 or 3.0; the generator downgrades the whole document. Converters also exist for Valibot (`@orpc/valibot`) and ArkType (`@orpc/arktype`); schemas without a matching converter fall back to Standard JSON Schema conversion.\n\nVerify the wiring before declaring success: request `/spec.json` under the handler prefix and one routed endpoint, and confirm the method, path, and status you configured.\n\n## Contract-first\n\nContract-first workflows belong to the `orpc-contract` skill: defining the shape with `oc` from `@orpc/contract`, implementing it with `implement` from `@orpc/server`, consuming the contract from clients, shipping it as minified JSON, and generating it from an existing OpenAPI spec with Hey API. On the OpenAPI side a contract behaves exactly like a router: attach routes with `.meta(openapi({...}))` on `oc` as shown in [Routing](#routing), serve the implemented router with `OpenAPIHandler` as usual, generate the spec document from the contract directly, and give `OpenAPILink` the contract as its runtime value.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/openapi/routing.md)\n\nPages to fetch when you need details beyond this skill:\n\n- OpenAPI: `openapi/routing`, `openapi/input-and-output-mapping`, `openapi/bracket-notation`, `openapi/serializer`, `openapi/handler`, `openapi/link`, `openapi/specification`, `openapi/scalar`\n- Plugins: `plugins/smart-coercion`, `plugins/openapi-reference`\n","skills/orpc/SKILL.md":"---\nname: orpc\ndescription: \"Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.\"\nlicense: MIT\n---\n\n# oRPC\n\noRPC is a typesafe API framework: write plain TypeScript functions on the server, call them from clients like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step. The same router can also be served as a REST API with an OpenAPI spec.\n\nThis skill targets oRPC v2. Check what is installed before writing code: `npm ls @orpc/server` (or any `@orpc/*` package). A 1.x version means v1, where this skill's guidance does not apply; use the `orpc-migrate` skill to upgrade. v2 currently ships under the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`; a plain install silently gets v1). If `npm view @orpc/server dist-tags` shows `latest` at 2.x, the beta has ended: install normally.\n\nPretrained oRPC knowledge describes v1 and is often wrong for v2 (routing moved to `.meta(openapi(...))`, `RPCLink` split `url` into `origin` plus a path, automatic middleware dedupe was removed). Prefer retrieval: the index of every docs page is at https://orpc.dev/llms.txt; see [Full documentation](#full-documentation) for the mechanics.\n\nPackage map:\n\n- `@orpc/server`: the `os` builder, routers, middleware, `RPCHandler`, server-side clients (`call`, `createRouterClient`), `implement` for mocks\n- `@orpc/client`: `createORPCClient`, `RPCLink`, `safe`, `createSafeClient`, `isDefinedError`\n- `@orpc/contract`: contract-first API definitions implemented separately from their logic (see the `orpc-contract` skill)\n- `@orpc/openapi`: `OpenAPIHandler`, `OpenAPILink`, OpenAPI 3.2 spec generation\n- Integration packages such as `@orpc/tanstack-query` and `@orpc/nest`\n\n## Define procedures\n\nBuild a procedure with the `os` builder: describe input with a schema, implement with `.handler`. Zod, Valibot, ArkType, and any other [Standard Schema](https://standardschema.dev/) library work for `.input`, `.output`, and error `data`.\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const listPlanets = os\n  .handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments\n\nexport const findPlanet = os\n  .input(z.object({ id: z.number() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n`.handler` is the only required step. The full chain, every other step optional:\n\n```ts\nconst example = os\n  .$context<{ headers: Headers }>() // initial context this procedure requires\n  .errors({ NOT_FOUND: {} }) // typed errors\n  .use(requireAuth) // middleware\n  .input(z.object({ id: z.number() }))\n  .output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking\n  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))\n```\n\nEvery builder step returns a new instance, so share base builders freely: `const authed = os.use(requireAuth)` then build many procedures from `authed`.\n\n## Assemble a router\n\nA router is a plain object mapping keys to procedures (or nested routers). Do not use the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n\n```ts\nexport const router = {\n  planet: { list: listPlanets, find: findPlanet },\n  admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree\n  planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router\n}\n```\n\nInfer types with `InferRouterInputs` / `InferRouterOutputs` from `@orpc/server`. Applying `.use` at both router and procedure level can run the same middleware twice; see the dedupe pattern below.\n\n## Middleware and context\n\nContext comes from two places. Initial context is declared with `.$context` and passed explicitly when serving or calling (environment values: headers, env, db). Injected context is added at runtime by middleware via `next({ context })` (runtime values: the authenticated user).\n\n```ts\nimport { ORPCError, os } from '@orpc/server'\n\nconst base = os.$context<{ headers: Headers }>()\n\nconst requireAuth = base.middleware(async ({ context, next }) => {\n  const user = await parseUser(context.headers)\n  if (!user) {\n    throw new ORPCError('UNAUTHORIZED')\n  }\n  return next({ context: { user } }) // handler now sees context.user, typed non-null\n})\n```\n\n`.use` accepts named middleware or inline functions. Middleware registered before `.input` runs before validation, the rest after. Middleware can also declare typed input (`os.middleware(async ({ next }, id: number) => ...)`); adapt mismatched shapes with `.use(mw.adaptInput(input => input.id))`.\n\nBest practice, dedupe expensive middleware: the same middleware can run twice in one call (router-level plus procedure-level `.use`, or a procedure `call`ing another). Cache the result in context:\n\n```ts\nconst authProvider = os\n  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()\n  .middleware(async ({ context, next }) => {\n    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)\n    return next({ context: { auth, authLoaded: true } })\n  })\n```\n\n## Typesafe errors\n\nThrow `ORPCError` (a `code` plus optional `message` and `data`). Both `message` and `data` are sent to the client, so never put secrets in them. Throw only `Error` instances, never literals.\n\nDefine errors with `.errors` so clients can infer each error's shape:\n\n```ts\nconst find = os\n  .errors({\n    NOT_FOUND: { message: 'Planet not found' }, // default message\n    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },\n  })\n  .handler(async ({ input, errors }) => {\n    throw errors.NOT_FOUND()\n  })\n```\n\n`throw new ORPCError('NOT_FOUND')` inside that handler is converted to the matching typed error when code and data match. Convert custom error classes to `ORPCError` in a middleware `try/catch`.\n\n## Serve with RPCHandler\n\n`RPCHandler` matches requests to procedures, validates input, runs handlers, and encodes results. Pick the adapter for your runtime. Fetch API (Bun, Deno, Cloudflare Workers):\n\n```ts\nimport { onError } from '@orpc/server'\nimport { RPCHandler } from '@orpc/server/fetch'\nimport { CORSHandlerPlugin } from '@orpc/server/plugins'\n\nconst handler = new RPCHandler(router, {\n  plugins: [new CORSHandlerPlugin()],\n  interceptors: [onError(error => console.error(error))],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, {\n    prefix: '/rpc',\n    context: { headers: request.headers }, // provide the router's initial context here\n  })\n  return matched ? response : new Response('Not found', { status: 404 })\n}\n// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workers\n```\n\nNode HTTP:\n\n```ts\nimport { createServer } from 'node:http'\nimport { RPCHandler } from '@orpc/server/node'\n\nconst handler = new RPCHandler(router)\n\nconst server = createServer(async (req, res) => {\n  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })\n  if (matched)\n    return\n  res.statusCode = 404\n  res.end('Not found')\n})\n\nserver.listen(3000)\n```\n\nUnmatched requests fall through to your own handling. By default `RPCHandler` accepts only `POST`, `PUT`, `PATCH`, and `DELETE`; enabling `GET` via `allowMethods` is a CSRF risk with cookie auth, see [RPC Handler](https://orpc.dev/docs/rpc/handler). Handler options also include `interceptors`, `routingInterceptors`, `clientInterceptors`, `plugins`, `filter`, and `errorStatusMap`. Adapters also exist for [AWS Lambda](https://orpc.dev/docs/adapters/aws-lambda), [Fastify](https://orpc.dev/docs/adapters/fastify), [WebSocket](https://orpc.dev/docs/adapters/websocket), [Message Port](https://orpc.dev/docs/adapters/message-port), and [Expo](https://orpc.dev/docs/adapters/expo).\n\n## Call procedures\n\nServer side (same process, no HTTP; also the fastest way to test procedures):\n\n```ts\nimport { call, createRouterClient } from '@orpc/server'\n\nconst planet = await call(findPlanet, { id: 1 }, { context: { headers } })\n\nconst client = createRouterClient(router, { context: { headers } }) // context can be a function\nconst planets = await client.planet.list()\n```\n\nClient side, `RPCLink` turns calls into HTTP requests. Import the router as a type only so no server code reaches the client bundle:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from '../server/router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({\n  origin: 'http://127.0.0.1:3000',\n  url: '/rpc', // must match the server's prefix\n  headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions\n})\n\nexport const orpc: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await orpc.planet.find({ id: 1 })\n```\n\nClient error handling: plain `try/catch` works, but `safe` preserves typed error inference:\n\n```ts\nimport { createSafeClient, isDefinedError, safe } from '@orpc/client'\n\nconst [error, data] = await safe(orpc.planet.find({ id: 1 }))\nif (isDefinedError(error)) {\n  console.log(error.code, error.data) // typed from the procedure's .errors\n}\nelse if (error) {\n  // unknown error\n}\n\nconst safeClient = createSafeClient(orpc) // every call returns [error, data]\n```\n\n## Beyond the basics\n\n- Streaming / SSE: return an async generator from `.handler`, validate events with `asyncIteratorObject`, resume via `lastEventId`: [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object)\n- OpenAPI: serve the same router as REST with routing metadata plus `OpenAPIHandler`, and generate a spec (covered in depth by the `orpc-openapi` skill): [OpenAPI Handler](https://orpc.dev/docs/openapi/handler)\n- Contract-first: define contracts with `@orpc/contract`, implement with `implement` (covered in depth by the `orpc-contract` skill): [Contracts](https://orpc.dev/docs/contract/procedure)\n- Plugins for handler and link: batch, CORS, dedupe, retry, compression, request limits, smart coercion, static files, timeout, tmp file upload, and more. Fetch a plugin's docs page before configuring it; option names are not guessable: [Plugins](https://orpc.dev/docs/plugins/batch)\n- Integrations: [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) (`createTanstackQueryUtils`), [SWR](https://orpc.dev/docs/integrations/swr), [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada), [Next.js](https://orpc.dev/docs/integrations/next), [NestJS](https://orpc.dev/docs/integrations/nest), [AI SDK](https://orpc.dev/docs/integrations/ai-sdk), [MCP](https://orpc.dev/docs/integrations/mcp) (`McpRegistrar` exposes procedures as MCP tools), [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry)\n- Testing: `call` procedures directly; mock with `implement(router.planet.list).handler(() => [])`, and run the project's typecheck before declaring success, since end-to-end types are oRPC's first correctness signal: [Testing and Mocking](https://orpc.dev/docs/recipes/testing-and-mocking)\n- Monorepos: TypeScript project references keep client types resolvable: [Monorepo Setup](https://orpc.dev/docs/recipes/monorepo-setup)\n- Migrating from tRPC or oRPC v1: use the `orpc-migrate` skill\n\n## Full documentation\n\nThis skill is an overview; fetch exact docs instead of guessing APIs, and if this skill and a fetched page disagree, trust the page. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/procedure.md)\n\nDoc map, all under `https://orpc.dev/docs/`:\n\n- Top level: `procedure`, `router`, `middleware`, `context`, `error-handling`, `metadata`, plus `binary-data` (file uploads) and `async-iterator-object` (streaming/SSE)\n- `rpc/*`, `openapi/*`: protocol details, handlers, links; `contract/*`: contract-first (the `orpc-contract` skill)\n- `client/*`: server- and client-side clients, error handling, `DynamicLink`\n- `adapters/*`: per-runtime serving quirks (fetch-api, node-http, aws-lambda, fastify, websocket, message-port, expo)\n- `plugins/*`: twenty handler/link plugins; `helpers/*`: cookie, encryption, form-data, lock, publisher, ratelimit, signing, base64url\n- `integrations/*`: framework glue; `recipes/*`: guidance (testing, SSR, monorepos, validation)\n- `migrations/from-v1`, `migrations/from-trpc`: upgrades (use the `orpc-migrate` skill)\n"},"files":{".agents/skills/blume-migrate/SKILL.md":"---\nname: blume-migrate\ndescription: Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, VitePress, VuePress, Docus, MkDocs, mdBook, Fern, GitBook, ReadMe, Redocly, Docsify, Jekyll (Just the Docs), GitHub Wiki, or any docs framework) to Blume, the markdown-first docs framework on Astro. Translate the source config to blume.config.ts, restructure content into Blume's filesystem-derived navigation, rewrite JSX callouts to directives, convert icons to Lucide, and inline snippets. Use when the user asks to migrate/convert/port a docs repo to Blume, or when the repo has a docs.json/mint.json, docusaurus.config.*, meta.json with fumadocs, _meta.* with nextra, an astro.config.* with starlight(), a .vitepress/config.* or .vuepress/config.*, an mkdocs.yml or zensical.toml, a book.toml (mdBook), a fern/docs.yml, a GitBook .gitbook.yaml/gitbook-docs.yaml, ReadMe _order.yaml files in docs/ or reference/, a Redocly sidebars.yaml or @redocly/realm dependency, an index.html that sets window.$docsify, a docus or @nuxt-themes/docus dependency, a Jekyll _config.yml (Just the Docs theme), or a GitHub wiki clone (Home.md with _Sidebar.md, [[Page Name]] links).\n---\n\n# Migrate to Blume\n\nBlume is a **markdown-first** documentation framework on Astro/Vite. You drop Markdown/MDX into a folder and get navigation, search, theming, Open Graph images, and a component library with no app boilerplate — **the framework is the template**. There is no starter to clone; the only thing a project owns is its content and a `blume.config.ts`.\n\nYour job is to convert a source docs repo into an **idiomatic** Blume project — not a 1:1 transliteration. Read this file, detect the source framework, open the matching `references/<framework>.md` for the exact mappings, and work the loop below. Report everything you drop or approximate.\n\nThroughout this skill (including the `references/` files), **`<skill>` means the absolute path of the directory containing this SKILL.md** — resolve it from wherever you read this file (e.g. `node_modules/blume/skills/blume-migrate` or `.claude/skills/blume-migrate`). It is a placeholder to substitute, never a literal path.\n\n## Migration philosophy\n\n- **Target idiomatic Blume, not a mechanical port.** Prefer filesystem-derived navigation over an exhaustive explicit `navigation.sidebar`. Prefer `:::` directives over JSX callouts. Prefer Blume defaults over restating them in config.\n- **Every field has a default; `{}` is a valid config.** Map only what the source _declares_. If the source uses a framework default, don't write it.\n- **Drop chrome that has no Blume equivalent — and say so.** Navbar CTAs, custom theming, conditional redirects, and unmappable icons get reported to the user, not silently discarded or faked.\n- **Convert, don't preserve.** Blume's page frontmatter schema is **strict** — unknown keys are build errors. A source-only frontmatter key must be mapped to a Blume key or removed (and reported), never left to \"maybe validate.\"\n\n## Migration workflow\n\n1. **Detect the source framework** and read its reference file:\n   - `docs.json` / `mint.json` → **Mintlify** (`references/mintlify.md`) — the deepest, config-declared nav.\n   - `docusaurus.config.*` → **Docusaurus** (`references/docusaurus.md`).\n   - `meta.json` + `fumadocs-*` deps (content under `content/docs/`) → **Fumadocs** (`references/fumadocs.md`).\n   - `_meta.{js,ts,json}` + `nextra` deps → **Nextra** (`references/nextra.md`).\n   - `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).\n   - `.vitepress/config.{js,ts,mjs,mts}` (often `docs/.vitepress/`) or a `vitepress` dependency → **VitePress** (`references/vitepress.md`). `.vuepress/` is VuePress (next bullet).\n   - `.vuepress/config.{js,ts,mjs}` (v1 also `config.yml`/`config.toml`; often `docs/.vuepress/`), a `vuepress.config.*`, or a `vuepress` dependency → **VuePress** (`references/vuepress.md`). It builds on `references/vitepress.md` for the Markdown layer the two share, so read both.\n   - A `docus` dependency (often in `docs/package.json` beside a `nuxt.config.ts` with `extends: ['docus']`), or `@nuxt-themes/docus` for 1.x → **Docus** (`references/docus.md`). Pages are MDC, Nuxt Content's component syntax: `::note` … `::` is not a Blume directive, and Blume prints it as text (`BLUME_MDC_SYNTAX` warns at its line).\n   - `mkdocs.yml`/`mkdocs.yaml` (also Zensical's `zensical.toml` and ProperDocs' `properdocs.yml`) → **MkDocs / Material for MkDocs** (`references/mkdocs.md`): nav declared in config, Python-Markdown extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets), and Python deps, so usually no `package.json`. Run `node <skill>/scripts/mkdocs-codemod.mjs --write <content-dir>` first, then `pin-heading-ids.mjs` after the first build.\n   - `fern/docs.yml` (beside `fern/fern.config.json`) → **Fern** (`references/fern.md`). A `fern/` folder without `docs.yml` only generates SDKs: the docs are elsewhere.\n   - `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, or a `SUMMARY.md` beside a `.gitbook/` folder → **GitBook** (`references/gitbook.md`). `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit (same reference, its own section); `book.toml` + `src/SUMMARY.md` is mdBook (`references/mdbook.md`).\n   - `book.toml` (at the book root, often `docs/`, `book/`, or `guide/` in a code repo; chapters in its `[book] src`, beside a `SUMMARY.md`) → **mdBook** (`references/mdbook.md`). Every URL ends in `.html`, `SUMMARY.md` is the whole nav, and `{{#include}}` pulls anchored code from across the repo: build the old book first, since it's the URL list, the old ids, and the only record of what the includes showed.\n   - `sidebars.yaml` or an `@redocly/realm`-family dependency, usually beside a `redocly.yaml` and with Markdoc `{% … %}` tags in pages → **Redocly** (`references/redocly.md`). GitBook pages use `{% … %}` tags too, so check for GitBook's files first. A `redocly.yaml` holding only lint/bundle keys (`extends`, `rules`, `decorators`, `apis`) is Redocly CLI config, not a docs site: keep it and detect the framework from the other signals.\n   - `_order.yaml` files in `docs/`, `reference/`, `recipes/`, or `custom_pages/` (ReadMe's bi-directional Git sync), or a CI step running `readmeio/rdme` beside pages with `category`/`parentDoc` (or `category.uri`/`parent.uri`) frontmatter (`rdme` uploads) → **ReadMe** (`references/readme.md`). ReadMe keeps settings, redirects, the glossary, and variables in its dashboard, not the repo, and changelogs only on the `main` branch.\n   - An `index.html` setting `window.$docsify` (usually in `docs/`, beside `_sidebar.md`), or `docsify-cli` in `package.json` → **Docsify** (`references/docsify.md`). Run `node <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old --group-folders <docs-dir>` first, then `pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json` after the first build. Its default URLs live after `#` (`/#/page`), which no server-side redirect can match; the reference adds a client-side script.\n   - `_config.yml` (root or `docs/`) → **Jekyll** (`references/jekyll.md`); Just the Docs is `just-the-docs` in `_config.yml` or the `Gemfile`. Build the old site first with a project-local bundle: the nav comes from front matter (`parent`/`nav_order`) and is rebuilt from the old build's sidebar, and URLs end in `/` or `.html`. A `_toc.yml` beside it is Jupyter Book, not Jekyll. Run `node <skill>/scripts/jekyll-codemod.mjs --old <old build> --write .` from the Jekyll source dir first, then `pin-heading-ids.mjs --old <old build>` after the first build.\n   - A clone of `<repo>.wiki.git` (`Home.md` at the root, often `_Sidebar.md` and `_Footer.md`, `[[Page Name]]` links, no config) → **GitHub Wiki** (`references/github-wiki.md`). Never detected: the source is named. Its old URLs stay on github.com, which can't redirect, so the reference writes a route table and link stubs for the old wiki instead of `redirects`. Run `node <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write` first, classify `wiki-routes.json`, then `convert --repo <owner>/<repo> --write`.\n   - Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.\n   - **Also note the host repo, independent of source framework:** a pnpm/Turbo workspace, a non-`docs/` content layout, or a Vercel deploy each need integration steps (`content.root` scoping, `minimumReleaseAge`, lockfile, `vercel.json`, an Astro/Vite patch) — all in `references/monorepo.md`. Read it whenever the target isn't a bare single-package docs folder.\n2. **Inventory the repo** before changing anything (a page whose body is only a `<meta http-equiv=\"refresh\">` is a redirect, not a page: add the redirect from its old URL and report it): the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs and GraphQL schemas, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted. Pages the old site published by accident (a `.github/` template, files under dot-folders, planning notes) aren't content: leave them out, add no redirects, and list them in the report.\n3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: \"…\" })`.\n4. **Restructure content.** Put `blume.config.ts` in the directory whose `package.json` runs the old docs build, which may be a workspace package such as `packages/docs` rather than the repo root. Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: \".\"` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** A group that exists only in the source's nav config, with its pages flat on disk, becomes a **`(group)/` folder**: it adds the sidebar group and keeps every URL, with no redirects (moving pages a folder deeper instead changes their URLs and breaks their relative asset paths). Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.\n5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; turn snippets/partials into `<include>` statements or inline them (Blume has no import-based includes: `import Snippet from \"…\"` plus `<Snippet />` has to become one or the other; when a source includes part of a file (mdBook `ANCHOR`s, VitePress `#region`s, MkDocs snippet line ranges) or a file outside the content root, copy `<skill>/scripts/include-excerpts.mjs` into the project and run it before every `dev`, `build`, and `validate` rather than pasting code the project compiles: it reports every missing anchor or region in one pass, and a restored marker in the source beats a line range, which goes stale silently); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources — and links inside OpenAPI descriptions, which Blume renders and validates, though a redirect `from` hides a stale one; rewrite links that hardcode the old site's own origin to root-relative paths); **add a `redirects` entry for every route you moved** in step 4 (a hash-routed source like Docsify also needs its reference's client-side redirect, because `redirects` only see paths); **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`), and fix links to its anchor (`#<h1-slug>`), which no longer exists: drop the fragment. `blume validate --strict` also reports fragments the old site never checked; fix each or drop it, and report it. A page with no frontmatter `title` takes its opening `# H1` as the title and shows it once, so that one can stay; moving it into `title` is still the idiomatic form. **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `githubReleases()` source** (see \"Changelogs\" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`). For **Docus**, run the bundled codemod first: `node <skill>/scripts/docus-codemod.mjs --write content` pairs MDC blocks as remark-mdc does, converts the Nuxt UI prose components, fence labels, icons, frontmatter, and `.navigation.yml` files, makes the MDX prose safe, and lists every `REVIEW` item (custom and landing-page components stay as written; check them against the live site, not only a local build; see `references/docus.md`). For **VitePress**, run the bundled codemod first: `node <skill>/scripts/vitepress-codemod.mjs --write <srcDir>` converts containers (paired as markdown-it pairs them), code groups, alerts, fence labels, `<<<` imports and includes, `.html` links, badges, and H1 titles, and lists every `REVIEW` item for you to finish (see `references/vitepress.md`). Before converting any source's components, check its built output: a component its theme never registered rendered nothing there (`<!---->`), so converting it shows content readers never saw; report it. For **mdBook**, convert every `{{#…}}` helper before anything else: anchored, line-range, and out-of-root code becomes generated excerpts, one-line files become `variables`; then strip hidden `# ` lines and `rust,…` fence attributes, and convert admonish blocks (see `references/mdbook.md`). For **Jekyll**, convert every Kramdown IAL (`{: .note }`) and Liquid tag: in `.md` they ship as literal text (a Liquid tag warns `BLUME_TEMPLATE_TAG`, an IAL `BLUME_MD_ATTRIBUTE_LIST`), and in `.mdx` they fail the build (`blume check` names each IAL's line as `BLUME_MDX_ATTRIBUTE_LIST` and a Liquid tag's as `BLUME_MDX_SYNTAX`, at the partial's own line for one in an included partial). Run the bundled codemod first: it converts callout IALs, the in-page TOC, H1s, front matter, Markdown includes, and Liquid links and variables, rebuilds the Just the Docs sidebar as folders, `slug`s, and `meta.ts`, and writes the redirects and variables to `jekyll-migration.json` (see `references/jekyll.md`). For a **GitHub Wiki**, run the bundled codemod (`routes`, then `convert`): it builds the route table (one flat route per page name) and converts every `[[…]]` link outside code, which Blume would otherwise render as literal text (`BLUME_WIKILINK_UNSUPPORTED` warns only for one that names a page or has a `|`; Markdown pages put the link text first: `[[text|Page]]`). For **Redocly**, convert every Markdoc `{% … %}` tag: Blume never parses them, so an unconverted tag ships as literal text in `.md` (`BLUME_TEMPLATE_TAG` warns at its line) and fails an `.mdx` build. For **VuePress**, rename every `README.md` to `index` first, then run the VitePress codemod (its limits on VuePress content are in `references/vuepress.md`), then grep for hyphenated theme tags (`<code-group>`, `<code-block>`, `<router-link>`): they pass through `.mdx` as unknown HTML elements with no diagnostic. For **MkDocs**, decide `.md` vs `.mdx` per page before converting: MDX rejects HTML comments, `<https://…>` autolinks, unclosed void tags, and `{…}` attribute lists that Python-Markdown accepts (`blume check` names each at its line: `BLUME_MDX_SYNTAX` for what MDX can't parse, and the last two as `BLUME_MDX_UNCLOSED_ELEMENT` and `BLUME_MDX_ATTRIBUTE_LIST`), and snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), not the page (see `references/mkdocs.md`). For **Fern**, URLs come from `docs.yml`, not file paths: run `node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt` before moving anything (dry run, then `--write`), and after the first build its `endpoints` step, which joins `operation-routes.mjs` output to Fern's old API URLs and writes `fern-redirects.json`; export a Fern Definition to OpenAPI with `fern export` (see `references/fern.md`). For **GitBook**, put each page at its old URL first (GitBook's URLs follow `SUMMARY.md`, not the file tree), then convert every `{% … %}` tag: in `.md` an unconverted tag ships as literal text (`BLUME_TEMPLATE_TAG` warns), and in `.mdx` it fails the build. For **ReadMe**, run the bundled codemod first: `node <skill>/scripts/readme-codemod.mjs --write --site <hub origin> --glossary <saved hub page> .` keeps every page at its flat URL, converts callouts, `doc:`/`ref:` links (`blume validate` names any left as `BLUME_UNSUPPORTED_LINK_SCHEME`), ReadMe components, and frontmatter, writes `meta.ts`, overlays, and redirects, and lists what needs a person; after the first build, its `--routes` pass adds the endpoint redirects and per-tag order (see `references/readme.md`). A pasted widget's JSX event handler (`onClick={…}`) builds green and never runs on a static page: `BLUME_MDX_EVENT_HANDLER` names each one, so move the widget into an island (`islands/`) or drop it and report it.\n6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. Yarn's `npmMinimalAgeGate` refuses a fresh `blume` the same way: add it to `npmPreapprovedPackages`. **Always regenerate the lockfile in the same change** with the repo's own package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, or `bun install`; check `packageManager` and the lockfile's name) and commit it alongside `package.json` — CI and hosts install from a frozen lockfile, so a stale one fails the build before it starts. After creating the first new file in the content root (a `meta.ts`, a partial), run `git check-ignore -v <content root>/meta.ts`: a repo `.gitignore` rule that covers the content root leaves every new file out of `git add` while the old pages stay tracked, so fix the rule or report that each new file needs `git add -f`. Blume needs **Node 22.19 or later**: update `.nvmrc`/`.node-version`, `engines.node`, and any CI `node-version` in the same change. pnpm 11 and later run a dependency's build script only once `pnpm-workspace.yaml` approves it: add `allowBuilds: { esbuild: true }` (plus `\"@scarf/scarf\": false`, as `blume init` writes) unless the workspace already lists `esbuild`, keeping its other entries. Add `.blume/`, `dist/`, and `.env.local` to the docs package's own `.gitignore` when it has one, else the root's (as `blume init` does; a `vercel()` build appends `.vercel/` itself), and remove any line that ignores `public/` before moving assets into it. **`blume build` empties `<project>/dist`:** if the host repo already uses `dist/` for something else (Python wheels, a library build), put the Blume project in its own folder (`docs/`). If the repo formats with Prettier, extend its `*.md` overrides (`.prettierrc`, `.editorconfig`) to `*.mdx` and keep `{/* */}` comments on one line. After renaming or moving pages, grep the whole repo (release config, CI, bot and ignore files) for their old paths. Check any `tsconfig.json` in the Blume project dir too: Blume's build reads it, so an `extends` that resolves only after a root workspace install, or `include`s of the old framework's folders, fail `blume build`. Install the workspace first, fix the paths, or delete the file if only the old framework used it (a Nuxt `tsconfig.json` that `extends` or `references` `./.nuxt/*` fails with `BLUME_TSCONFIG_EXTENDS`, and a `postinstall: nuxt prepare` script goes with the Nuxt dependency). **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.71.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.\n7. **Wire up the host repo & deploy (non-trivial repos).** Settings a host keeps in its dashboard (Netlify's publish directory, build command, `NODE_VERSION`) can't be committed: list them in the summary. In `.gitignore`, replace only the docs' own entries and keep ones other packages use. For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`: with `outputDirectory: \"dist\"` for a static build, and with no `outputDirectory` for `deployment: vercel()`, whose build writes `.vercel/output` (add `.vercel/output/**` to Turborepo outputs). Tell the user the settings you can't commit (Vercel Root Directory, Node 22, and server secrets such as `AI_GATEWAY_API_KEY`). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.\n8. **Verify.** Run `blume build` (frontmatter schema, duplicate routes, config — it fails on any error diagnostic by default; **never pass `--no-strict`**, which builds anyway and silently drops pages with invalid front matter or `.mdx` that doesn't parse) and `blume validate --strict` (internal links, heading anchors, assets, redirect targets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).\n\n## The Blume mental model\n\nThe single biggest shift for most sources — especially Mintlify — is that **navigation is derived from the filesystem**, not declared in config.\n\n### Navigation is the file tree\n\n- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.\n- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).\n- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display, directory })`). The `pages` array lists children by slug (numeric prefix, parentheses, and extension stripped; an entry that names nothing warns `BLUME_META_UNKNOWN_PAGE` with the slug it likely meant, so write that one); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order. `directory` inherits down the tree: set `directory: \"none\"` on nested folders that shouldn't list cards. `pages` can't interleave loose pages with subgroups inside a `flat` group, or at the top of the sidebar in any mode: when a source interleaves them (mdBook, MkDocs `navigation.sections`), use `display: \"group\"`, where the order holds inside groups, and say what moved at the top level.\n- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `\"flat\"` (default), `\"group\"` (collapsible), or `\"page\"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).\n- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).\n- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting, **use a `(group)/` folder first**: move the pages into `<folder>/(<group>)/` with a `meta.ts` for the label, order, and `collapsed`. A parenthesized folder adds the sidebar group but no URL segment, so no route changes and no redirects are needed, and a page from another folder can join with a `slug` that keeps its old route. A group whose landing page is a sibling file keeps every URL too: move the landing page to `(group)/index.md` with `slug: <its old route>`, and the slugged index becomes the group's link. Moving a page a folder deeper breaks its relative links and assets (`./img.png`, `../other.md`), so re-base them. (Docsify is the exception: its old `#/` URLs are remapped by script whatever the paths, so its reference moves pages into real folders.) Only when that can't express the shape, move the pages into a real subfolder (URLs change: add `redirects`) or declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.\n\n### Tabs and selectors\n\n- **`navigation.tabs`** (`{ label, path, icon? }`) render top-of-header sections and **scope the sidebar by route** — the folder at a tab's `path` becomes the section, so this needs no config beyond the tabs themselves; structure content as **one folder per tab**. **A source's top-level tabs (Mintlify `navigation.tabs`, a top-level product/section switcher) map to these header tabs — keep them as tabs; don't flatten them into a single global `navigation.sidebar`.** Blume picks the active tab by **URL prefix** (longest tab `path` that prefixes the route), so every page in a tab must live under that tab's single `path`; a source tab that mixes arbitrary routes isn't portable as-is — either move its pages under one prefix (route change → add `redirects`) or accept the closest shape, and say which in the report (details in `references/mintlify.md`). The filtering runs both ways: on a route **under** a tab's `path`, the sidebar shows **only** that tab's folder (a tab also highlights when the current route is under it); on a **root or untabbed** route (or a tab whose `path` is `/`), the tab folders are **hidden** and the sidebar shows only the loose pages that belong to no tab (full tree as a fallback, so it's never blank). Consequence for migrations: once you add tabs, the landing sidebar automatically drops the sectioned content — that's intended, not lost pages; don't hand-build excludes for it.\n- **`navigation.selectors`** (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`, `kind` = `dropdown`/`product`/`version`/`language`) partition a whole site (products, versions) via a header dropdown keyed on the current route.\n- **`navigation.actions`** (`[{ label, href }]`) puts plain links in the **header**, left of the icon buttons, and **`navigation.cta`** (`{ label, href }`, singular) is the header's one filled call-to-action button. This is the home for a source's header bar — a \"Log in\"/\"Status\" link goes in `actions`, a \"Sign up\"/\"Get started\" button in `cta`. An `http(s)` href opens in a new tab; an internal route is validated against your pages at build time, so a route served by another app on the same host must be written as an absolute URL. Both hide on phones (below `sm`), except `cta` on a page with no navigation toggle; a link that must survive on a phone on docs pages belongs in `featured`.\n- **`navigation.featured`** (`{ label, href, icon? }`) pins links to the **top of the sidebar, above every section** — a blog, changelog, or support page that should always be one click away. These are the **exception to tab scoping**: unlike the generated tree, featured links show on **every** route and breakpoint. `href` points anywhere — an external URL opens in a new tab, an internal route (`/contact`) is validated against your pages at build time. `icon` is a Lucide name (or image path/URL/inline SVG), as everywhere else. This is the home for a source's always-visible header/utility links (Mintlify anchors, Blog/Contact links) — see `references/mintlify.md`.\n\n### Routes and pathing\n\n- A route is the content path relative to `content.root`, with **numeric prefixes stripped** (`01-intro.mdx` → `/intro`; a version or a name that starts with a date, `YYYY-MM-DD`, `D-M-YYYY`/`M-D-YYYY`, or `YYYY-MM`, stays whole: `12-05-2022.mdx` → `/12-05-2022`; pin `slug` for any other digit-led name the old site kept whole) and **`(group)/` folders adding no segment**. An `index` file maps to its folder's route. Frontmatter `slug` overrides the generated route. A `README.md` is an ordinary page at `/README`, not its folder's index: rename it `index.md` (Docsify, MkDocs, GitBook, and VuePress sources all use README as the index).\n\n### `blume.config.ts` shape\n\n`defineConfig({...})` — every field optional, all with defaults:\n\n- **Site:** `title`, `description`, `logo` (string SVG, or `{ image: string | { light, dark, alt }, text, href }`), `banner` (a string, or `{ content, link: { href, text }, dismissible, id }` — no color/type). A logo renders beside `title` in the header, so a **wordmark logo doubles the brand** (\"Acme Acme\") — set `text: \"\"` to render the mark alone. **Prefer the string form over `{ light, dark }`:** if you have the logo SVG locally and it's monochrome (solid black or white), rewrite its `fill`/`stroke` to `currentColor` and use `logo: \"/logo.svg\"` — it then inherits the theme's text color and adapts to light/dark automatically, so you don't need separate light/dark files.\n- **`theme`:** `accent` (a color string for both modes, or `{ light, dark }` per mode), `action` (color), `mode` (`light`/`dark`/`system`), `radius`, `fonts` (`{ body, display, mono }` — each a curated Google-font slug, a `{ name, provider?, weights? }` object for any Google/Fontsource/Bunny/Fontshare family, or `{ name, variants: [{ src, weight?, style? }] }` for local font files), `background` and `backgroundImage` (each a string, or `{ light, dark }` per mode). The old `accentDark`/`backgroundDark`/`backgroundImageDark` fields were **merged into these per-mode objects** — a bare string still applies to both modes, so only reach for `{ light, dark }` when the two modes differ. Each color must be a CSS color or an accent preset (a source's bare `\"3b82f6\"` needs its `#`), or `BLUME_THEME_COLOR_INVALID` warns and browsers ignore it. There is **no** `theme.strict` and **no** `theme.css` config field — custom CSS goes in a project-root **`theme.css` file** (auto-picked-up), and a source's \"strict appearance\" flags drop.\n- **`content`:** `root` (default `\"docs\"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `[\"**/*.{md,mdx}\"]` / `[\"**/_*\", \"**/.*\"]` — a set `exclude` **adds to** those defaults, so partials and dot-folders stay unpublished; to publish `_`-prefixed or dot-files on purpose, list the default with a `!`: `exclude: [\"!**/_*\"]`), `sources` (an array of **adapters imported from `blume/sources`**: `filesystem({ root, include, exclude })`, `obsidian({ vault })`, `githubReleases({ owner, repo })`, `notion({ database })`, `sanity({ projectId, dataset, query })`, `contentful({ space, contentType })`, `payload({ url, collection })`, `strapi({ url, contentType })`, `mdxRemote({ github })`, `custom(source)`; every factory with an options object also takes `prefix` and `pollInterval`, while `custom(source)` takes a `ContentSource` instance that sets its own `prefix`. The 1.x `{ type: \"…\" }` objects were removed — rename `type` to the factory call and pass the other fields as its options — except `{ type: \"custom\", source }`, which becomes `custom(source)` with the instance as the only argument. `root`/`include`/`exclude` are shorthand for a single `filesystem()` and are **rejected beside `sources`** — move them into the `filesystem()` entry. OpenAPI/AsyncAPI/GraphQL are **not** among these; they're adapters in the top-level `reference` list), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.\n- **`basePath`** (top-level): a site-wide mount point (e.g. `\"/docs\"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).\n- **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the footer's GitHub link when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see \"Config-declared nesting\" above).\n- **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: \"algolia\", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (the assistant, Open in chat — `ai.assistant.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openai({ model })` (or `openai({ baseUrl, name, model, apiKeyEnv })` for any OpenAI-compatible endpoint), `anthropic({ model })`, `gemini({ model })`, `grok({ model })`, `openrouter({ model, reasoning })`, `llmgateway({ model })`, or `inkeep({ model })`. Every adapter takes `model`, `apiKeyEnv`, `headers`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`). Every adapter except `inkeep()` also takes `reasoning`: Inkeep runs its own answer pipeline, so `inkeep({ reasoning })` fails validation with an unrecognized key — drop a source's reasoning setting there and report it. There are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.assistant` — a source that configured an AI assistant that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` sit on `ai.assistant` itself; there is **no** `ai.ask` — Blume 2.0.0 and earlier used that name, so an older `blume.config.ts` moves the whole block to `ai.assistant`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).\n- **`analytics`** — adapters from `blume/analytics`, including `script()` for a source's custom head scripts. A known analytics snippet in the source's head scripts (the gtag loader plus `gtag('config')` pair, GTM, Plausible, …) becomes that provider's factory, not `script()`. Adapters load **only in production builds and wait for `consent`** when it's configured, so a script a page needs to work (a support widget, a form embed) goes in a `PageFooter` layout slot component instead (`defineComponents`).\n- **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: \"static\"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (the assistant, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from \"blume/deploy\"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: \"/docs\" }` (see below for when to add `site`). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from \"blume/deploy\"`) when a config uses both.\n- **Keep the source's site URL as `deployment.site` unless the target host is one Blume auto-detects.** Blume fills `site` in from the platform's build environment only on **Vercel and Netlify** (their stable production domain), and uses the dev server's `localhost` URL during `blume dev`; on those two hosts leave it unset and let detection pick the deployed URL. Everywhere else — Cloudflare Pages (it exposes only the per-deploy `CF_PAGES_URL`), GitHub Pages, S3 or another static host, a custom CDN, Cloudflare Workers, a `node()` server — nothing detects it, and an unset `site` silently drops the sitemap, OG images, RSS feeds, the AI catalog, and absolute canonical URLs. So when the source config had a `url`/`site` field and the target isn't one of those two hosts, carry it over: `deployment: { site: \"https://…\" }` for a static build, or the `site` option of a host adapter (`node({ site })`). If you can't tell where the site will deploy, keep it and say so in the report.\n- **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png` or `apple-touch-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.\n\nThe schema is exported from `blume/schema`; the full field reference is in the `docs/configuration/` directory of the installed `blume` package (see \"Full documentation\" below for how to locate it).\n\n### Icons are Lucide, period\n\nBlume resolves **bare kebab-case [Lucide](https://lucide.dev) names** everywhere an icon is accepted — frontmatter `icon`, `sidebar.icon`, `meta.ts` `icon`, `navigation.tabs`/`selectors` icons, and `Card`/`Step`/`Icon`/etc. props. There is **no** FontAwesome or Tabler support and **no** `iconType` prop. Names must be **kebab-case** (`book-open`, not `BookOpen`) — a PascalCase React-component name (common in Fumadocs/lucide-react sources) does not resolve and renders nothing. When migrating a source that uses another icon set (Mintlify and Fern use FontAwesome), **map each name to its closest Lucide equivalent**; where none exists, drop the icon and report it. Verify a name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it.\n\n### Page frontmatter (strict — unknown keys are build errors)\n\n```yaml\n---\ntitle: Install # renders as the page H1 — remove any duplicate H1 in the body\ndescription: Install Blume and scaffold your first project.\ntype: doc # doc (default); blog and changelog drive feeds, other values only mean something under content.types\nsidebar:\n  label: Install # overrides title in the sidebar\n  order: 2\n  icon: download\n  badge: New\n  hidden: false\nseo:\n  title: …\n  description: …\n  image: /og/install.png\n  canonical: https://…\n  noindex: false\nsearch:\n  exclude: false\n  tags: [api]\nslug: install # override the generated route\ndraft: false\nlastModified: 2026-06-20 # pin the \"last updated\" date\n---\n```\n\nAlso valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog metadata), `deprecated`, and the top-level shorthands `hidden` (for `sidebar.hidden`) and `noindex` (for `seo.noindex`).\n\n### Authoring features (no imports needed in `.mdx`)\n\n- **The rich features are MDX-only.** Directives, `package-install`, mermaid, and math are wired into the MDX processor; in a plain `.md` file a `:::note` stays **literal text** — and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. **Rename any `.md` file that uses (or should use) these to `.mdx` during migration.** MDX is stricter prose than Markdown: any `<` followed by a non-space character that doesn't open a tag (`<7`, `<50 ms`, `<= 5`, `List<T>`) and a bare `{` or `}` fail the build, so escape them (`&lt;` or `\\<`, and `\\{`) in any page you rename. With `lastModified: \"git\"`, every page the migration edits shows the migration commit's date, so say so in the report when the site shows dates; a page it only renames or moves keeps its date (Blume follows renames). This bites hardest on Docusaurus/Starlight sources, whose `.md` content is full of `:::` admonitions. Plain Markdown (headings, tables, fenced code with titles/highlighting) is fine in `.md`.\n- **Callouts as directives:** `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::info`, `:::success`, with an optional title in brackets, which renders its inline Markdown: `:::warning[Heads up]`. Aliases `caution`→warning, `error`→danger, `important`→note, `warn`→warning. A callout that needs its own icon or tint is `<Callout type icon color title>` (a Lucide `icon`, any CSS `color`), which a directive can't set. **markdown-it sources (VitePress, VuePress, Docusaurus v2) write `::: tip Title`: with a space after the colons it's never a directive, even in `.mdx`, and renders as literal text (`.mdx` warns `BLUME_DIRECTIVE_SPACED_NAME` only for a callout name); inside an open `:::` container it closes that container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`); a title after the name (`:::tip Title`) is dropped too (`BLUME_DIRECTIVE_OPENING_TEXT`).** Normalize to `:::tip[Title]`. MDC's two-colon blocks (Docus, any Nuxt Content site: `::note` … `::`) are leaf directives to Blume and print as text (`BLUME_MDC_SYNTAX`), and MDC `[text]{.class}` spans fail the MDX build (`BLUME_MDX_ATTRIBUTE_LIST` names the line). GitHub alerts (`> [!NOTE]`, any of GitHub's five, `[!CAUTION]` as danger) render as callouts in `.mdx`, text after the marker as the title; in `.md` they stay quotes that show the marker (`BLUME_MD_GITHUB_ALERT`), so rename those pages to `.mdx`.\n- **No-import MDX components:** `Callout`, `Card`/`CardGroup`, `Columns`/`Column`, `Steps`/`Step`, `Tabs`/`Tab`, `Accordion`/`AccordionItem`, `Expandable`, `FileTree`, `Tree`/`Tree.Folder`/`Tree.File`, `CodeGroup`, `Frame`, `Panel`, `Tooltip`, `Tile`, `Badge`, `Icon`, `TypeTable`/`AutoTypeTable`, `Color`, `YouTube`, `Visibility`, `GithubInfo`, `Component`, `CodeBlock`, `Diff`, `Prompt`, `Math`, `Changelog`, and the hand-written endpoint fields `ParamField`/`ResponseField`/`RequestExample`/`ResponseExample` (Mintlify's semantics: `<ParamField path|query|header|body=\"name\">` names the field by its location; see `docs/references/api-pages.mdx`). (**Not** shipped — convert away: `<Warning>`/`<Note>`/`<Info>`/`<Tip>`/`<Check>`/`<Error>` → the matching `:::` directive, and `RequestField`. A `<CodeBlock>` with no `code` prop, used as a wrapper around a fence as Fern and Mintlify do, renders the fence as written but ignores its own `title`: unwrap the fence and put the title in its meta. A component that shows its children, used with none and another tool's props (`<Badge type=\"tip\" text=\"beta\" />`), renders empty and warns `BLUME_UNKNOWN_PROP`: put the content between the tags. See the reference files for targets.)\n- **Fenced-code superpowers:** ` ```package-install ` → package-manager tabs; ` ```mermaid ` → a rendered diagram; code-block titles (` ```ts server.ts `), line numbers (`lineNumbers`), and highlighting (`{1,4-5}`, `// [!code ++]`).\n- **Fences and titles that trip migrations:** a fence language Shiki doesn't know renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE` (case doesn't matter: `JSON` highlights like `json`): fix the id, or write `text` for plain text. A language glued to attributes with a comma (` ```rust,ignore `) is one of those; with a space instead, rustdoc's and mdBook's keywords (`ignore`, `no_run`, `should_panic`, `edition2021`, `editable`, …) warn `BLUME_CODE_FENCE_OPTION` and stay out of the title, but any other second word becomes the block's title: keep only the language. Other tools' fence options (`hl_lines=\"2 3\"`, `linenums=\"1\"`, `showLineNumbers`, Mintlify's `lines`, `wordWrap`, `filename=\"…\"`) do nothing, stay out of the title, and warn `BLUME_CODE_FENCE_OPTION` with the Blume spelling (`{2,3}`, `lineNumbers`, `wrap`, `title=\"…\"`). A fence with options but no language needs one (` ```text title=\"x\" lineNumbers `), or the first option is read as the language. A title is every word after the language, and a bracketed one (` ```ts [file.ts] `, from VitePress and MDC) drops its brackets, with a range glued after them (`[file.ts]{2}`) highlighting its lines. `Expandable`, `AccordionItem`, and callout titles render inline Markdown (code, emphasis, links); a `Step` title is a plain string that shows Markdown as written and has no anchor, so keep an anchored heading as the first line inside `<Step>`. Raw HTML `<img src>` and `<a href>` are never optimized: a relative one naming a file beside the page publishes that file with the page (as `[spec](./spec.pdf)` and `<Card img=\"./cover.png\">` do), a root path gains `deployment.base` like a Markdown link, and `blume validate` checks both, but a relative `<a href>` to a page resolves against the page's URL, not its folder. Convert images to Markdown so they're optimized, and page links to Markdown links or root routes.\n- **Math:** `$$…$$` renders in `.mdx` with **no conf\n\n<!-- [TRUNCATED AT 50KB] -->",".agents/skills/blume-migrate/references/docsify.md":"# Docsify → Blume\n\nDocsify has no build step. One `index.html` sets `window.$docsify = {…}`, loads Docsify and its plugins from a CDN, and renders each page's Markdown in the browser. Watch for five traps:\n\n- **URLs live after a `#`** (`/#/guide/setup?id=install`). A server never sees that part, so `redirects` can't match an old URL. The site needs a client-side redirect script (URLs below).\n- **Links resolve from the docs root**, not the page's folder, unless `relativePath: true`. Images and includes resolve from the page's folder either way, even with a leading `/`. Raw HTML `href`/`src` resolve from the docs root, because the document URL is always the folder holding `index.html`.\n- **Pages have no frontmatter.** The title is usually the first `# H1`, but not always.\n- **Navigation is a Markdown list** (`_sidebar.md`), and its groups usually sit flat in one folder.\n- **Heading ids differ from Blume's**, and old deep links carry them as `?id=`.\n\n**Fingerprint the version.** It changes callouts and heading ids. `docsify@4`, or any script URL with `/lib/` (even an unversioned `…/npm/docsify/lib/docsify.min.js`), runs v4 code: the v5 package still ships the v4 build under `lib/`. `docsify@5/dist/…` or `new Docsify({…})` is v5.\n\n## Detect\n\n- An **`index.html`** whose inline script sets **`window.$docsify`** (v5 can also use `new Docsify({…})`), usually in `docs/`. The folder holding it is the content root.\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `.nojekyll` beside it, with `README.md` as the home page.\n- `docsify-cli` or `docsify` in `package.json`. Without either, the source has to be named: `npx blume migrate docsify`.\n- A GitHub wiki has `_Sidebar.md` and `Home.md` but no `index.html`. It isn't Docsify.\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that runs `docsify serve <dir>` (usually the repo root), so the default `content.root: \"docs\"` matches. With no manifest, scaffold one there.\n\nRead the whole `index.html`: the `$docsify` object (it can be a function), every `<script src>` (each is a plugin), the `<head>`, and the `<body>`. Open a few routes on the live site too. **A page whose include target is missing rendered blank in Docsify 4**, so some \"pages\" never worked.\n\n## Run the codemod first\n\nOn the Docsify folder, from the directory that will hold `blume.config.ts`, dry-run the bundled codemod, then apply it once, before any hand edits:\n\n```bash\ncurl -s https://api.github.com/emojis > emojis.json   # only if pages use :shortcodes:\nnode <skill>/scripts/docsify-codemod.mjs docs\nnode <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old \\\n  --group-folders --emoji emojis.json --site https://docs.example.com docs\n```\n\nIt's zero-dependency and idempotent, and reads `version`, `relativePath`, `routerMode`, `noEmoji`, `homepage`, `loadSidebar`, `loadNavbar`, and `coverpage` from `index.html` (override with `--version` and `--relative-path`). It converts:\n\n- `!>` → `:::warning`, `?>` → `:::tip`, and `> [!TYPE]` alerts → directives, leaving fenced and indented code alone;\n- docsify-tabs → `<Tabs>`;\n- `':include'` lines → `<include>`, resolved from the page's folder as Docsify did, even with a leading `/`. A `:fragment=` is copied to its own file where Docsify cut one: code includes, plus Markdown ones in v5. Docsify 4 showed a Markdown include whole, so it stays whole;\n- `:id=` → `[#id]`, `{docsify-ignore}` → `[!toc]`, and setext headings → ATX;\n- links resolved the way Docsify resolved them (hash links, `?id=`, and `--site` absolute links included) → relative file links, without an anchor that named the target's title H1 (that heading becomes `title`);\n- images → rebased for moved pages. Docsify read a leading-slash image from the page's folder too, where Blume would read it from `public/`;\n- link and image attribute strings → dropped;\n- relative raw HTML URLs and `':ignore'` links → root paths. An `':ignore'` link to a page's own file opened the raw Markdown, so it becomes a raw `<a href>` to the page's Markdown copy (`/<new route>.md`): a Markdown link naming a page's file, even root-relative, lands on the page itself;\n- emoji shortcodes → Unicode;\n- the H1 or sidebar label → `title`, plus `sidebar.label`, `seo.title`, and `hidden: true`, with later H1s demoted;\n- `README.md` → `index.md`, and pages that need MDX → `.mdx` with MDX-safe comments, `<br>`, void tags, and autolinks.\n\n`--group-folders` moves each page into a folder per sidebar group, writes each folder's `meta.ts` in sidebar order plus a root `meta.ts` that keeps the groups in sidebar order, and writes the old → new route table to `docsify-routes.json`. Leave the flag off to keep the files where they are. With `homepage: \"intro.md\"`, the table also maps `/intro` → `/`, since Docsify served that page at both.\n\n`--snapshot` records every page's headings with the ids Docsify gave them, plus `old-routes.txt`. The pin step (URLs below) needs it, and it only works on the first run, while the pages are untouched. On Zenroom it matched Docsify 4.13.1's rendered ids on every page that rendered.\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each. The codemod never invents a fix: a missing include stays as written, and a file that must move to `public/` is only listed.\n\n## Config: `window.$docsify` → `blume.config.ts`\n\n| Docsify | Blume |\n| --- | --- |\n| `name` (may hold HTML) | `title`, as plain text. The `<title>` and `<meta name=\"description\">` in `index.html` give `title` (when `name` is unset) and `description` |\n| `logo` | `logo`, with the file moved to `public/`. Docsify shows the logo instead of the name, so a wordmark needs `logo: { image, text: \"\" }`. Check it in dark mode |\n| `nameLink` | `logo.href` |\n| `repo` | `github: { owner, repo }` (with `branch` from an edit-on-github URL). A non-GitHub repo goes in `navigation.actions` |\n| `themeColor` (v4), `--theme-color` in a stylesheet (v5) | `theme.accent` |\n| A dark-only theme (`dark.css`, a themeable `*-dark.css`) | `theme.mode: \"dark\"`. Other themes map to nothing |\n| Fonts in the stylesheet or `<link>`s | `theme.fonts` |\n| `<link rel=\"icon\">` | Copy the file to `public/favicon.<ext>` |\n| `<meta>` verification tags | `seo.metatags`. `twitter:site` goes to `seo.x.handle`. Drop `og:*` and `twitter:` card tags, since Blume writes them |\n| Custom CSS | A root `theme.css` with only the rules whose markup still exists. Docsify selectors (`.markdown-section`, `.sidebar`, `.cover`) match nothing |\n| Site URL (`CNAME`, a sitemap) | `deployment.site`. GitHub Pages isn't auto-detected |\n| `subMaxLevel: N` | `toc: { maxHeadingLevel: N }` when N ≥ 2 and isn't 3. Drop `maxLevel` |\n| `homepage: \"x.md\"` | Handled by the codemod. A remote URL needs vendoring |\n| `alias` | Local-to-local entries go in the route table. Remote targets: see Includes and aliases. Drop sidebar plumbing (`'/.*/_sidebar.md'`) and any alias whose target doesn't exist |\n| `externalLinkTarget: '_blank'` (explicit) | `markdown: { externalLinks: true }`. Docsify opens external links in a new tab even when this is unset, so report the change |\n| `notFoundPage` / `_404.md` | Blume's 404 page, or a custom `pages/404.astro` |\n| `formatUpdated`, `{docsify-updated}`, a last-modified plugin | `lastModified: \"git\"` (and `fetch-depth: 0` in CI) |\n| `collapsibleSidebarGroups` (v5) | `navigation.sidebar.display: \"group\"`, plus `collapsed: false` in each `meta.ts` that should start open |\n| `vueComponents`, `vueMounts`, `vueGlobalOptions`, `executeScript` | An island per widget (`islands/Name.vue`, `docs/content/islands.mdx`). Report |\n| `markdown` (marked options or a `renderer`) | Drop, and report what the renderer did. Common ones are mermaid (now native) and a `json` pretty-printer: minified JSON includes now render as one line, so fix that in whatever generates them. `breaks: true` has no equivalent |\n| `routes` (virtual routes) | Static ones become pages. Report dynamic ones |\n| `auto2top`, `topMargin`, `maxLevel`, `cornerExternalLinkTarget`, `requestHeaders`, `noCompileLinks`, `sidebarPosition`, `keyBindings`, `skipLink`, the search and pagination options, … | Drop |\n\n**Plugins** are `<script>` tags:\n\n- **Built in:** search, `docsify-copy-code`, `docsify-pagination`, and `zoom-image`. Delete them.\n- **Converted by the codemod:** emoji, `docsify-tabs`, and flexible-alerts.\n- **Prism language packs:** delete them. Shiki highlights the common languages in any case (` ```JSON ` works), and a fence in a language it doesn't know, or a misspelled one, renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: fix the name, or write `text`.\n- **Mermaid** plugins or renderers → native ` ```mermaid ` in `.mdx`.\n- **KaTeX** → `$$…$$` in `.mdx`, with inline `$…$` becoming `$$…$$` inside its sentence.\n- **`ga` / `gtag`** → `googleAnalytics({ id })` for a `G-` id. A `UA-` id no longer collects data, so report it. Other analytics → `script()`.\n- **Comments** (Disqus, Gitalk, giscus) → nothing built in. Use a `PageFooter` slot, or report them.\n- **Anything else** (bibtex, remote-markdown, demo boxes, custom inline plugins) → report.\n\n## Navigation: `_sidebar.md` → folders and `meta.ts`\n\n- **Groups over pages in one flat folder** (everything in `docs/` or `docs/pages/`) flatten silently. Use `--group-folders`: it moves each group's pages into a folder and writes `meta.ts` and the route table, and the hash script maps every old route. This departs from SKILL.md's `(group)/`-first rule: every Docsify URL changes form anyway (`/#/x` → `/x`), so the script runs for each old link either way, and the codemod rebases the moved pages' links and images. To keep URLs instead, move pages at the content root into `(group)/` folders, which add no URL segment; under a subfolder like `pages/`, they keep `/pages/x` but nest every group under a \"Pages\" group. Or keep the files and write an explicit `navigation.sidebar`.\n- **Groups whose pages already sit in a folder named for the group** (`Guide` → `guide/`) stay put, and `--group-folders` writes that folder's `meta.ts`. It moves the pages of a group whose folder has another name into one named for the group, so on a site already laid out in folders, leave the flag off and write each `meta.ts` by hand, with the group label and the sidebar's order.\n- **The home page** stays the root `index`. The generated sidebar lists it first, never inside a group.\n- **Loose pages listed after groups** can't keep that order, because Blume lists loose pages first. Move them into a folder, or report the change.\n- **External links, and links to files** like a PDF or HTML in `public/`, become `navigation.featured`. A file link there warns at build (\"no page matches it\"); the warning is harmless.\n- **A `_sidebar.md` inside a folder** replaced the sidebar under that folder. Map the folder to a `navigation.tabs` entry.\n- **Pages the sidebar doesn't list** get `hidden: true` from the codemod. They stay reachable, as before.\n- **Sidebar link titles** (`[x](x.md \"Tab title\")`) become `seo.title`. Report any that look copy-pasted from another page: Docsify showed them as the browser tab title.\n\n## Navbar and cover page\n\n- **`_navbar.md`:** doc-section links become `navigation.tabs` (a nested list becomes a tab with `items`), and other links become `navigation.actions`. With `mergeNavbar: true`, put the other links in `navigation.featured` instead, not in both: featured shows on every screen size, while actions hide on phones. Language links (`/zh-cn/`) become `i18n`.\n- **`_coverpage.md`** (shown above `README.md` on `/`; the whole home page with `onlyCover: true`): rebuild it as the top of `index.md`. The `# name <small>1.2</small>` heading is already the title. The tagline is prose or `description`, the logo is a Markdown image, and the buttons become links or `<Card>`s. Drop the background image or `![color](#…)`, and report it. Use `mode: center` when the cover was the whole page.\n\n## Content: what the codemod leaves to you\n\n**Titles.** Docsify drops only the first H1 from its sidebar headings, so pages often have several. The codemod takes a leading H1 as the title and demotes every later heading from the second H1 on. When content comes before the first H1, it takes the sidebar label as the title and demotes everything. Check every title it flags: about one page in five on Zenroom started with a section (\"Key generation\", \"Intro\"). For those, use the sidebar label as the title, put the H1 back as an `##` section, and demote every heading.\n\n**MDX hazards** in a renamed page. The codemod lists each one, and all but the first fail the build:\n\n- An indented code block renders as a plain paragraph: fence it.\n- An HTML table whose cell tags share a line with text: give each `<tr>`/`<td>` its own line.\n- A block element inside `<p>`, typical of README banners: make the `<p>` a `<div>`.\n- A repeated attribute: keep one.\n- A brace in prose: escape it as `\\{`.\n\n**Includes and aliases.**\n\n- **A missing include target:** the page was blank on the old site. Repoint the include to the file's new name (a folder was usually renamed), or drop it, and report it.\n- **A fragment copied from a generated file** goes stale. Have the generator write one file per fragment.\n- **An included file's own H1** becomes a second `<h1>`: remove it.\n- **An alias to a raw URL of a file in the same repo** (`'/CHANGELOG': 'https://raw.githubusercontent.com/<owner>/<repo>/main/CHANGELOG.md'`) becomes a page that `<include>`s the file through a symlink in `_snippets/`. Removing that file's H1 changes a file people read on GitHub, so say so.\n- **Other remote aliases and includes:** vendor a copy (with a source comment), or use `mdxRemote()`.\n- **iframe, video, and audio embeds** become raw HTML, with the file in `public/`.\n\n**Files the pages link** (listed under \"must be served from public/\"): Docsify served every file in its folder at its path. Blume publishes a file a page links relatively with that page, but at a new URL, and the codemod writes Docsify's root-resolved links as root paths, which keep the old URLs and are served from `public/`.\n\n- Copy each file into `public/` at the same path.\n- If a generator writes the files (test fixtures, examples), symlink the folder instead: `public/_media/examples → ../docs/examples`. Astro copies the target, and `validate` checks the links.\n- An HTML document saved as `.md` (generated API docs) goes to `public/` as HTML.\n- `blume validate --strict` reports a missing one as `BLUME_BROKEN_ASSET`.\n\n**Everything else:**\n\n- **Link attributes** the codemod dropped (`:target`, `:size`, `:no-zoom`, `:class`): restore any that matter as raw HTML (`<img src=\"/x.png\" width=\"200\" data-no-zoom />`, with the file in `public/`).\n- **Emoji shortcodes left over:** paste the emoji.\n- **Page `<script>`s:** Docsify 4 ran them only with `executeScript` or Vue, so they often never ran. Move one that did into an island.\n- **Vendored READMEs** keep Blume's ids (they had GitHub-style ids already), and they aren't in the snapshot.\n\n## URLs and redirects\n\n**Hash mode (the default).** Every page lived at `/#/<route>`, also reachable as `/#/<route>.md` and `/index.html#/<route>`, with folder indexes at `#/<folder>/` and headings at `?id=<id>`. The server only saw `/`.\n\n**A. Blume takes over the old address** (the usual case). `docsify-routes.json` holds the page moves the codemod made. Add an entry for each alias you turned into a page. Routes that now land on a file in `public/` go in a separate `files` map that only the script reads: a redirect page would sit where the file is. Name a `public/` folder with its slash (`/api/`) or name the file itself (`/api/index.html`): static hosts, `blume dev`, and `blume preview` serve its `index.html` at `/api/` (and redirect `/api` there), where its relative links resolve.\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport moved from \"./docsify-routes.json\" with { type: \"json\" };\n\n// Old routes that now land on a file in public/: the script sends readers\n// there, but a redirect can't, so they stay out of `redirects`.\nconst files = {}; // e.g. { \"/onepage\": \"/onepage.md\" }\n\n// Docsify served every page at /#/<route>; a server never sees the hash.\n// Swap an old hash URL for its new route, and ?id=<heading> for #<heading>.\nconst base = \"\"; // the site's deployment.base plus basePath, if it has either\nconst docsifyRedirects = `(() => {\n  const base = ${JSON.stringify(base)};\n  const routes = ${JSON.stringify({ ...moved, ...files })};\n  const go = () => {\n    const hash = location.hash;\n    if (!hash.startsWith(\"#/\")) return;\n    const q = hash.indexOf(\"?\");\n    let path = q === -1 ? hash.slice(1) : hash.slice(1, q);\n    const id = q === -1 ? null : new URLSearchParams(hash.slice(q + 1)).get(\"id\");\n    try { path = decodeURI(new URL(path, \"http://x\").pathname); } catch {}\n    path = path.replace(/\\\\.md$/i, \"\").replace(/(^|\\\\/)README$/i, \"$1\");\n    if (path.length > 1) path = path.replace(/\\\\/+$/, \"\");\n    // location.origin keeps a crafted #//other.host/ link on this site.\n    location.replace(location.origin + base + (routes[path] ?? path) + (id ? \"#\" + id : \"\"));\n  };\n  go();\n  addEventListener(\"hashchange\", go);\n})();`;\n\nexport default defineConfig({\n  redirects: Object.entries(moved).map(([from, to]) => ({ from, to })),\n  integrations: [\n    {\n      name: \"docsify-hash-redirects\",\n      hooks: {\n        \"astro:config:setup\": ({ injectScript }) => {\n          injectScript(\"head-inline\", docsifyRedirects);\n        },\n      },\n    },\n  ],\n});\n```\n\nThe script runs from every page's `<head>`, the 404 page included, before the body loads, in `blume dev` and in every build. Keep its two safeguards: `new URL(…).pathname` resolves the `./` and `../` segments Docsify left in links (`#/./guide`), and `location.origin` stops a crafted `#//other.host/` from sending readers off the site. Unmoved routes need no entry. The `redirects` also cover direct visits to a moved path and its `/old.md` Markdown twin. Don't use the `script()` analytics adapter for this: it's emitted in production builds only, and held until consent when `consent` is set.\n\n**B. The old site keeps its own address** (an old GitHub Pages URL, while the docs move to a new domain). Replace the old `index.html` with a page running the same parsing, with the new origin in place of `location.origin`, and a `<noscript><meta http-equiv=\"refresh\" content=\"0; url=https://new.example/\"></noscript>` fallback.\n\n**History mode** (`routerMode: 'history'`). URLs were real paths, so the route table and `redirects` carry moved pages. Anchors were still `?id=`, so add this after `go();`:\n\n```js\nif (!location.hash) {\n  const id = new URLSearchParams(location.search).get(\"id\");\n  if (id) location.replace(location.pathname + \"#\" + id);\n}\n```\n\n**Keep old heading anchors.** Docsify 4 leaks entity names into ids (`Foo & Bar` → `foo-amp-bar`, `Don't` → `don39t`), collapses dash runs, keeps emoji and shortcode names, and prefixes a leading digit with `_`. Docsify 5 is closer to Blume, but still differs in that prefix, in non-ASCII capitals, and where a heading has emphasis (`_When_` stays in the id) or a shortcode. After the first build, pin each old id where it differs, and rebuild until it reports 0:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json --write\n```\n\nOn Zenroom, one pass kept every old id except those of title H1s (58 of 968), and a link to one of those lands at the top of the page anyway. A page that never rendered on the old site (a missing include) has nothing to pin.\n\n**Check every old route** against `blume preview`. In `.docsify-old/old-routes.txt`, a folder route is listed without its slash, but Docsify served it only with one. Both forms reach the script.\n\n```bash\nnode -e '\nconst routes = { ...require(\"./docsify-routes.json\") };   // add the files map too\nconst old = require(\"fs\").readFileSync(\".docsify-old/old-routes.txt\", \"utf8\").split(\"\\n\").filter(Boolean);\n(async () => { for (const r of old) {\n  const res = await fetch(\"http://localhost:4321\" + (routes[r] ?? r));\n  if (res.status !== 200) console.log(res.status, r);\n} })();'\n```\n\nThen open a few old hash URLs, with and without `?id=`, in a browser on the deployed site. A plain `curl` can't test the hash script.\n\n**GitHub Pages**, where most Docsify sites live:\n\n- Switch the Pages source from the branch folder to GitHub Actions, using the workflow in https://useblume.dev/guides/markdown-docs-github-pages (with `fetch-depth: 0` for `lastModified: \"git\"`).\n- Pages has no redirect rules of its own. A static build writes each exact `redirects` entry as a page that forwards to the new URL, which is all the table holds; a pattern redirect wouldn't work there.\n- A project URL (`user.github.io/repo/`) becomes `deployment: { site: \"https://user.github.io\", base: \"/repo\" }`, and that base goes in the script too. Blume adds the base to root paths in Markdown and raw HTML alike, so the codemod's root paths need nothing more.\n- The custom domain stays in the Pages settings. Neither `CNAME` nor `.nojekyll` is needed.\n- The workflow's `npm ci` needs a lockfile, so commit one.\n\n## Teardown\n\nAfter harvesting what they declare, delete:\n\n- `index.html`;\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `_404.md` (Blume ignores `_` files meanwhile), or the files `loadSidebar`, `loadNavbar`, and `coverpage` name instead. Without the `_`, Blume publishes them as pages until they're gone;\n- `.nojekyll`, the theme CSS, and generators that read the sidebar or Docsify syntax (sitemaps, one-page builds).\n\nSwap `docsify-cli`/`docsify` for `blume` and `docsify serve` for `blume dev`/`blume build`/`blume preview`. Keep `docsify-routes.json` and `.docsify-old/` until the pins are done and old links stop mattering.\n\n## Dropped — report these\n\n- The cover's background and the navbar's language links (now `i18n`).\n- The home page's place in a sidebar group, and loose pages after groups.\n- Link and image attributes you didn't restore.\n- Fragments now frozen as copies.\n- Vendored or `mdxRemote()` aliases.\n- Pages that were blank on the old site (missing includes) and what you did with each.\n- Title H1 anchors (they land at the page top).\n- Comment plugins, a `UA-` analytics id, Vue widgets, and page scripts.\n- Custom `markdown` renderers (JSON pretty-printing included) and `breaks: true`.\n- Dynamic virtual `routes`.\n- Theme CSS with no Blume target, and copy-pasted sidebar link titles now in `seo.title`.\n- New-tab external links, unless you set `markdown.externalLinks`.\n",".agents/skills/blume-migrate/references/docus.md":"# Docus → Blume\n\nDocus is a Nuxt layer for docs sites. It's built on Nuxt Content (pages are Markdown files in `content/`, routed by their path) and Nuxt UI (the theme, and the prose components pages use). Navigation is the file tree plus per-folder `.navigation.yml` files, and settings live in `app.config.ts` and `nuxt.config.ts`. Pages are written in **MDC**, Nuxt Content's Markdown-with-components syntax. The bundled codemod converts most of it; these traps are why it exists:\n\n- **MDC's `::note` … `::` looks like a Blume directive and isn't one.** Blume prints `::note` and its `::` closer as text (in `.md`, every MDC construct), and `blume check` and `build` warn `BLUME_MDC_SYNTAX` at each block opener and inline component (`:badge[New]{color=\"primary\"}`) left in either format: convert them as below.\n- **MDX is stricter than MDC.** `[text]{.class}` spans fail the build, and so do `{…}`, `<Name>`, and `<50` in prose. Prompt bodies are full of these.\n- **Icons are Iconify names** (`i-lucide-zap`, `i-simple-icons-github`). Blume renders nothing for them, and warns only for sidebar and tab icons.\n- **Docus sorts siblings by file path as text,** so `10.faq.md` lists between `1.` and `2.`. Blume sorts the prefixes as numbers.\n\nScope:\n\n- **Docus 3.x–5.x** is covered: the `docus` package on Nuxt Content 3 and Nuxt UI, whose content syntax is the same across the three. 5.x is current and is by far the most installed, and it's the one this reference was run on.\n- **Docus 1.x** (`@nuxt-themes/docus`, on Nuxt Content 2 with Nuxt Elements components) is flagged. Its section near the end was written from the package source, not tested on a site, and the codemod doesn't convert its components.\n- **The Nuxt UI docs template** (`nuxt-ui-templates/docs`, no `docus` dependency) writes the same content: the codemod and this reference apply, and its `app.config.ts` is read by hand.\n\n## Detect\n\n- **A `docus` dependency** means 3.x–5.x, and **`@nuxt-themes/docus`** means 1.x. It's usually in the docs package's own `package.json` (`docs/`, `apps/docs/`) in a module's monorepo.\n- Usually a `nuxt.config.ts` with `extends: ['docus']`. The 4.x starters have no config and run `nuxt dev --extends docus`, and 3.x ran `docus dev`.\n- A `content/` folder beside it with `.navigation.yml` files (3.x+) or `_dir.yml` files (1.x).\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that depends on `docus`, and set `content.root: \"content\"`. `public/` stays where it is: both tools serve it at the site root. In a workspace, read `references/monorepo.md` too.\n\n- **No `content/docs/` folder:** every file under `content/` is a page, and `content/index.md` is the landing page at `/`.\n- **A `content/docs/` folder:** Docus publishes only `content/docs/**`, under `/docs/…`, plus `content/index.md`. Exclude anything else under `content/` and report it. Docus hides the `docs` level in its sidebar. In Blume, add a tab `{ label: \"Docs\", path: \"/docs\" }`, or the section tabs below, or the sidebar wraps everything in a \"Docs\" group.\n- **`content/<locale>/` folders:** see i18n.\n\n## Record the old site first\n\nBefore editing anything, save the URL list, the old heading ids, and the record of what rendered:\n\n- **URLs:** the live `/sitemap.xml` (on an i18n site, an index of one sitemap per locale), plus every `routeRules` redirect source in `nuxt.config.ts`.\n- **HTML:** `nuxt generate` writes `.output/public`, one `<route>.html` per page plus a meta-refresh stub per redirect. Copy it outside the repo; it's `<old build>` below. Or point the anchor script at the live origin.\n- **Check components against the live site.** A local build can miss a component a Nuxt module registers in its own build: it shows as an empty tag (`<InstallButton …></InstallButton>`) while production renders it. Request live pages with a browser user agent (`curl -A 'Mozilla/5.0'`): some Docus deployments answer `curl` with Markdown.\n\n## Run the codemod first\n\nOn the `content/` directory, dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/docus-codemod.mjs content          # report only\nnode <skill>/scripts/docus-codemod.mjs --write content  # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent. Icon names are checked against Blume's Lucide set once `blume` is installed (or with `--lucide <icons.json>`).\n\n**What it does:**\n\n- **Pairs MDC blocks** as remark-mdc does (see MDC syntax).\n- **Converts the Nuxt UI prose components:**\n  - callouts → `:::type` directives, or `<Callout type icon>` when a callout's icon isn't its type's own\n  - `card-group`/`card`, `field-group`/`field` → `<ResponseField>`, `steps`, `tabs`, `accordion`, `collapsible`\n  - `code-group` → `<CodeGroup>`, or one `package-install` fence when the group only repeats an install command\n  - `code-collapse`, `code-tree`, `code-preview`, `prompt`, `badge`\n  - the inline `:icon`, `:kbd`, `:badge`, `:video`, and `:u-color-mode-image`\n- **Strips Markdown attributes:** `[text]{.class}` spans and `{…}` attributes go.\n- **Rewrites fence labels and frontmatter:** fence `[label]`s become titles, `navigation` → `sidebar`, `seo.ogImage` → `seo.image`, `links` → `related`, a body H1 → `title`, and `.navigation.yml` → `meta.ts`.\n- **Renames to `.mdx`** each page that now needs MDX, and makes its prose MDX-safe (see MDC syntax).\n\nEvery `REVIEW` line is your to-do list, including:\n\n- custom and landing-page components, left as written\n- blocks with a slot the conversion doesn't read (a card's `#footer`), left as written\n- card descriptions, which Nuxt UI's prose card never rendered\n- callouts that linked somewhere\n- icons outside Lucide\n- unclosed blocks and stray closers\n- `links` lists longer than `related` takes, and frontmatter keys Blume rejects\n- MDC blocks inside blockquotes\n- file names whose URL changes\n\n## Config: `app.config.ts` → `blume.config.ts`\n\nRead `app/app.config.ts` (`app.config.ts` at the project root on Nuxt 3), `nuxt.config.ts`, the CSS that `css` lists, and `.env.example`.\n\n| `app.config.ts` | Blume |\n| --- | --- |\n| `seo.title` | `title`. Docus falls back to `site.name` in `nuxt.config.ts`, then the `package.json` `name`, then the git repository's name, so carry the one the site showed |\n| `seo.description` | `description` (Docus falls back to the `package.json` `description`) |\n| `seo.titleTemplate` | drop. The default `%s - <site>` is Blume's; report a custom one |\n| `seo.schema` | drop (report). Blume writes its own JSON-LD |\n| `header.title` | `title`, when there's no logo |\n| `header.logo.{light,dark,alt}` | `logo: { image: { light, dark, alt }, text: \"\" }`. Docus shows the logo without the title. A monochrome SVG → `currentColor` and the string form (SKILL.md) |\n| `header.logo.display: 'wordmark'` | the `wordmark` files as the logo |\n| `socials` (Simple Icons slug → URL) | `footer.socials` for the platforms Blume lists (`twitter` → `x`). Others (`nuxt`, `npm`) → `footer.links`, or drop (report) |\n| `github` (`url`, `branch`, `rootDir`), or nothing | `github: { owner, repo, branch, dir }`. **Docus reads the git remote when `github` isn't set,** so edit links showed anyway: take `owner`/`repo` from the remote. `dir` = `rootDir`; `branch` only when it isn't `main`. `github: false` → leave it out |\n| `toc.bottom.links` | `navigation.featured` (`to` → `href`). Report the move |\n| `navigation.sub` | `navigation.tabs` (see Navigation). `'aside'` anchors have no Blume form; report |\n| `docus.locale` (not `en`) | a one-locale `i18n` block |\n| `docus.colorMode` | `theme.mode`. Blume always shows the toggle; report |\n| `assistant.faqQuestions` | `ai.assistant.suggestions` (`[{ label }]`; flatten categories) |\n| `ui.colors.primary` | `theme.accent`. Nuxt UI paints shade 500 in light mode and 400 in dark: a Blume preset of the same name, else `{ light, dark }` with those Tailwind values. **Not declared means Docus's emerald:** the site turns Blume's default color unless you offer `theme.accent: \"green\"`; report it either way |\n| `--ui-radius: 0` in CSS | `theme.radius: \"none\"` |\n| `toc.title`, `docus.shortcuts`, `search.fts`, other `assistant.*` and `ui.*` keys | drop (report the `ui` overrides) |\n\n| `nuxt.config.ts` | Blume |\n| --- | --- |\n| `site.url` / `NUXT_SITE_URL` | `deployment.site`, unless the host is Vercel or Netlify (SKILL.md) |\n| `routeRules` with `redirect` | `redirects` (see URLs and redirects) |\n| `@nuxtjs/i18n` and `i18n` | see i18n |\n| `llms.notes`, `llms.sections` | `agents.llmsTxt: { details }` as Markdown, if worth keeping; `llms.title`/`description` drop |\n| `mcp` | Docus serves an MCP server at `/mcp` (or `mcp.route`) unless `mcp: false` or `mcp.enabled: false`. To keep one: `agents.mcp: { enabled: true, name }` (and `route` when it isn't `/mcp`) plus a host adapter (server output). Blume's tools are fixed, so report the site's own tools and prompts in `server/mcp/` |\n| `docus.assistant` | Docus turns the assistant on when the build has `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`: check the live site for it. Keep it as `ai: { assistant: { enabled: true, provider: gateway({ model }) } }` (`gateway` from `blume/ai`; Docus's default model is `google/gemini-3-flash`) plus a host adapter, or drop it (report) |\n| `app.head`, `@vercel/analytics`, `@nuxtjs/plausible`, `@nuxt/scripts` analytics | the provider's factory from `blume/analytics` (`vercel()`, `plausible({ domain })`, `cloudflare({ token })`); other scripts → `script()`; `meta` → `seo.metatags`. `@vercel/speed-insights` has no adapter (report) |\n| `content.build.markdown` (`toc`, `highlight.theme`, plugins), `mdc.highlight.theme` | `toc` and `markdown.code.theme` only when they differ from the defaults; each remark/rehype plugin's syntax converted by hand (report) |\n| `css` | what still applies → a root `theme.css` against Blume's tokens |\n| `studio`, `agentDiscovery`, `ogImage`, `sitemap`, `robots`, `schemaOrg`, `image`, `icon`, `nitro`, `vite` | drop (report). Blume writes its own sitemap, robots, OG images, and JSON-LD |\n\n**A project `content.config.ts`** adds collections such as a blog or changelog. Those aren't docs pages: map a blog to `type: blog` and a changelog to `type: changelog` (or `githubReleases()`, SKILL.md Changelogs), and report anything else.\n\n**Theme overrides** (`AppHeader*.vue`, `AppFooter*.vue`, `Docs*.vue`) aren't content. Map what they add to config (`navigation.cta`, `navigation.actions`, `footer.links`, `banner`), but read their `v-if`s first: a button shown only on the landing page (`route.path === '/'`) belongs on the landing page, since Blume's `cta` shows site-wide.\n\n## Navigation\n\nThe codemod writes each `.navigation.yml` as a `meta.ts` (`title`, `icon`) and each page's `navigation` as `sidebar` (`title` → `label`, `icon`, `false` → `hidden`). What's left:\n\n- **A folder's `navigation: false`** hides it: set `sidebar.hidden: true` on its pages (the codemod reports it).\n- **A folder with an `index.md` but no `.navigation.yml`:** Docus labels it with the index page's title. Blume uses the humanized folder name, so set `meta.ts` `title` when they differ.\n- **Order:**\n  - Docus compares paths as text: `1.intro`, `10.faq`, `11.glossary`, `2.setup`. Blume sorts the numbers. In a folder with ten or more unpadded siblings, keep Blume's order (almost always what the author meant) and report the change.\n  - Docus also interleaves pages and subfolders. Blume lists a level's pages above its subfolders, so report a page that sorted after a folder.\n- **Display:** Docus draws every folder as an always-open group, which is Blume's default `flat` display.\n- **`navigation.sub`** (a header tab bar, or anchors in the sidebar) makes each top-level folder a section that scopes the sidebar. That's `navigation.tabs`: one `{ label, path, icon? }` per top-level folder with pages, with `label` and `icon` from its `.navigation.yml`. A section with no index page needs no `href`, since a Blume tab falls back to the section's first page, as Docus did.\n\n## Pages: frontmatter and titles\n\nThe codemod maps `navigation`, `links` (header buttons → `related` cards at the foot of the page), and `seo.ogImage`. It removes `layout: docs` and `sitemap`. What's left:\n\n- **No `description`:** when a page opens with a paragraph (after an optional `# H1`), Docus took it as the description, so it showed twice. Blume derives none. To keep the meta description without the repeat, set `seo.description` to that paragraph (optional; say which you did).\n- **`sitemap: false`** has no sitemap-only switch: add `seo: { noindex: true }` only when the page should also leave search engines.\n- **`toc: false`** → `mode: wide` (report), or drop. Another `layout` → a Blume `mode`.\n- **Other keys** Nuxt Content kept and Blume rejects: drop and report each one, or declare it with `frontmatter.extend` if a component you wrote reads it.\n- **Titles:** Docus renders frontmatter `title` in the page header, falling back to the first `# H1`, so a body H1 showed twice. The codemod moves a lone H1 into `title` and reports the rest.\n\n## MDC syntax\n\nThe codemod applies these rules; you need them for what it leaves. They're remark-mdc's behavior (checked against `remark-mdc@3.11.1`):\n\n- **Block component:** an opening line `::name`, optionally followed by `{attributes}` and nothing else, then Markdown, then a closing line of colons only.\n  - Props are inline (`{key=\"value\" flag .class #id}`) or a YAML block, from `---` right after the opener to the next `---`. A `:key='[…]'` holds JSON.\n  - Slots are `#name` lines inside the block.\n  - An inline component is `:name[text]{attrs}`; alone on a line, it renders as a block.\n- **Pairing:**\n  - Every opener nests, even at the same colon count.\n  - A closer closes the innermost open block **whose opener has exactly as many colons**, together with anything still open inside it. It must not be indented deeper than that opener. Any other colon line, or a `::` mid-line, is text.\n  - An unclosed block runs to the end of its parent, and a closer inside a code fence is code.\n  - Check the old HTML for a literal `::` to see what the live site already showed broken.\n- **MDX safety:** in a `.mdx` page, prose must escape `{` and `}` as `\\{` and `\\}`, and turn **any `<` followed by a non-space character** into `&lt;` (or `\\<`) unless it opens a real tag: `useFetch<User>()`, `<script setup>` written as text, `<50 ms`, `<= 5`. Case matters as in JSX: `<input>` is HTML, but `<Input>` is a component, so a generic like `useTool<Input>()` is text. HTML comments become `{/* … */}`, `<https://…>` autolinks become links, and `<br>` becomes `<br />`. Code spans and `$$` math stay as written.\n- **Code fences:** ` ```ts [file.ts]{2} ` → ` ```ts file.ts {2} ` (`title=\"Two words\"` for a label with spaces). Blume reads ` ```ts [file.ts]{2} ` and a multi-word title as written too, the range highlighting its lines, so this only tidies. Leave MDC samples inside fences alone: they're documentation.\n\n## What the codemod leaves\n\n- **Custom components:** `app/components/content/*.vue`, or a Nuxt module's. Read the SFC to see what it renders and which props and slots it reads, then convert it:\n  - to Markdown\n  - to a Blume component\n  - to a static `.astro` component registered in `components.ts`\n  - to the `.vue` file as an island in `islands/`, with `@astrojs/vue` and `vue` installed. `:items='[\"a\"]'` becomes `items={[\"a\"]}`, and named slots become props or children.\n\n  Report each one. A component that rendered nothing on the live site (an unknown tag) has nothing to convert.\n\n- **Callouts with `to`:** the whole callout was a link. Add the link to its text when it isn't there already.\n- **Card descriptions:** Nuxt UI's prose card renders neither a `description` prop nor a `#description` slot, so that text never showed. The codemod keeps it as the card's body and flags it: keep it, or delete it to match the old page.\n- **Blocks with a slot the conversion doesn't read** (a card's `#footer`, a tab's extra slot) stay as written: convert them by hand.\n- **`code-tree`** becomes a `<CodeGroup>` titled by file path. The tree view is gone; a `<FileTree>` before it can show the layout.\n- **`prompt`:** Blume has no `windsurf` or `claude` action, and no prompt icon (the codemod reports both).\n- **Prose about features that didn't carry over:** grep the content for the dropped MCP tool and prompt names, `docus`, `/.well-known/skills`, and `Studio`, and list every page that now says something false.\n\n## Landing page\n\n`content/index.md` renders full-width, with no sidebar, title, or table of contents. Its `title` and `description` feed only the meta tags. The codemod leaves its `u-page-*` and custom components as written.\n\n- **Markdown landing** → `index.mdx` with `mode: custom` (no title or sidebar rendered, so write the heading) and `sidebar.hidden: true` (Docus never listed it).\n- **A designed landing**, or `app/pages/index.vue` → `pages/index.astro` on `PageLayout`.\n\n| Nuxt UI | Blume, in the `mode: custom` page |\n| --- | --- |\n| `u-page-hero` (`title`, `description`, `links`, or slots) | `# Title`, a paragraph, and a line of Markdown links; the primary one can also be `navigation.cta` |\n| `u-page-section` | `## Title`, a paragraph, and its features as cards |\n| `u-page-feature`, `u-page-card` (`title`, `description`, `icon`, `to`) | `<Card title icon href>description</Card>` in `<CardGroup cols={3}>` |\n| `u-page-grid`, `u-page-columns` | `<CardGroup cols={3}>` |\n| `u-button`, `u-page-cta` | Markdown links; a heading and a paragraph |\n| `u-page-logos`, decorative `div`s (glows, gradients) | a row of images, or drop (report) |\n\n## Icons\n\nBlume resolves Lucide names only (`zap`, or `lucide:zap`); **anything else renders nothing.** The codemod converts `i-lucide-*` (aliases of renamed icons included) and reports the rest:\n\n- **Other sets** (`i-heroicons-*`, `i-ph-*`): the closest Lucide name, checked at [lucide.dev/icons](https://lucide.dev/icons).\n- **Brands** (`i-simple-icons-*`): Lucide has a few (`github`, `gitlab`, `slack`, `linkedin`, `youtube`), but **`i-simple-icons-x` is not Lucide's `x`, a close cross.** Otherwise, save the SVG under `public/` and pass its path (`icon=\"/icons/nuxt.svg\"`), or drop it (report).\n- **`i-custom-*`** (Docus's `app/assets/icons/*.svg`): copy the SVG to `public/icons/` and pass the path.\n\n## i18n\n\nDocus i18n is `@nuxtjs/i18n` (`defaultLocale`, `locales: [{ code, name }]`) plus one `content/<code>/` folder per locale, and it forces the `prefix` strategy: every locale is under its code, the default included, and `/` redirects to the default.\n\n- Move `content/<default>/` up to `content/` (a default-locale folder warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`). Keep the others as `content/<code>/`, and run the codemod on `content/`.\n- Write `i18n: { defaultLocale, locales: [{ code, label }], hideDefaultLocalePrefix: false }`, `label` = `name`. That keeps every `/en/…` URL. Nothing serves `/` then, so add `{ from: \"/\", to: \"/<default>\", status: 302 }` as Docus did.\n- **Regional codes are lowercase in Docus URLs** (`zh-TW` → `/zh-tw/…`): use the lowercase code as Blume's `code` and folder.\n- Leave out any configured locale with no `content/<code>/` folder: Docus skipped it.\n\n## URLs and redirects\n\nDocus routes a page by its path under `content/`. It strips an `N.` prefix from each segment, drops `index`, and slugifies (lowercase, spaces → `-`). Blume strips `N.`, `N-`, and `N_` prefixes and `index` the same way, so **most URLs carry over unchanged**, with no trailing slash or `.html` to redirect. The codemod reports the exceptions:\n\n- **A dash prefix** (`01-intro.md`) stays in the Docus URL and Blume strips it: pin `slug` or redirect.\n- **Uppercase letters or spaces** were lowercased by Docus: rename the file.\n- **A version name** (`1.x`, `2.0`) stays whole in Docus: pin `slug`.\n- **`_` files** are pages in Docus and excluded by Blume: rename them.\n\n**Redirects:**\n\n- **`routeRules` → `redirects`.** A string `redirect` is a 307 in Nitro (`status: 307`); `{ to, statusCode }` → `{ to, status }`. A `/**` key strips its base: `'/old/**': { redirect: '/new/**' }` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`.\n- **`/raw/<route>.md`** is the Markdown copy of each page, which Docus links from `llms.txt`. Blume serves it at `/<route>.md`, so one rule covers it: `{ from: \"/raw/:path*\", to: \"/:path*\" }`. A `routeRules` redirect between two `/raw/…md` paths should target Blume's copy directly (`/raw/old.md` → `/new.md`), or it chains through that rule.\n- **Agent skills:** a `skills/` folder Docus published under `/.well-known/skills/` → `agents.skills: \"./skills\"`. Blume publishes them under `/.well-known/agent-skills/`, a skill with references as one `.tar.gz`. Redirect the old index to the new one, and report the change. Links can name the new index (`/.well-known/agent-skills/index.json`): `blume validate` accepts the files Blume generates.\n- **Anchors:** MDC slugs headings like Blume (`github-slugger`), then collapses repeated dashes, trims edge dashes, and prefixes a leading digit with `_`. `## Name & Title` is `#name-title` in Docus and `#name--title` in Blume; `## 1. Install` is `#_1-install`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old <old build>`, `--write`, and rebuild until it reports 0. On a server build (`vercel()`, `node()`) it reads the pages from `dist/client` itself.\n- **Verify** every old URL: a built page, or a redirect `from` whose target is built (SKILL.md Verification).\n\n## Deploy and teardown\n\n- **Keeping the assistant or the MCP server** needs server output: `deployment: vercel()` (or `node()`, `netlify()`, `cloudflare()`). `blume preview` can't serve a `vercel()` build, so check it with `blume dev`, then a preview deployment.\n- **Vercel with `vercel()`:**\n  - The build writes the Build Output API folder `.vercel/output` in the Blume project, not `dist/`.\n  - In the project's `vercel.json`, set `\"framework\": null` and the install and build commands (`references/monorepo.md` §4 for a workspace), and **no `outputDirectory`**. `framework: null` overrides a project still set to the Nuxt preset.\n  - In the dashboard, set the Root Directory to the docs package and Node to 22.19 or later. Set `AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token.\n  - With Turborepo, add `.vercel/output/**` to the docs' build outputs.\n  - List the dashboard settings as steps for the user.\n- **Dependencies:** remove `docus`, `nuxt`, the `@nuxt/*`/`@nuxtjs/*` modules, `nuxt-studio`, `better-sqlite3`, `@iconify-json/*`, `tailwindcss` (if only the theme used it), mapped analytics packages, `@nuxt/eslint-config`, and `vue` (keep it with `@astrojs/vue` for islands). Keep what the site's tests or scripts still import.\n- **Scripts:** `dev`/`build`/`generate`/`preview` → `blume dev`/`build`/`build`/`preview`. **Delete `postinstall: nuxt prepare`**, which fails once Nuxt is gone.\n- **Files:** after harvesting, delete `nuxt.config.ts`, `app/`, `content.config.ts`, `server/` (report its routes), and a Nuxt `eslint.config.mjs`. The codemod leaves a `.navigation.yml` whose keys `meta.ts` can't hold.\n- **`tsconfig.json`:** a Nuxt one points into `.nuxt/`, and `blume build` then fails with `BLUME_TSCONFIG_EXTENDS`. Delete or replace it.\n- **`.gitignore`:** in the docs package's own file, `.nuxt`, `.output`, `.data` → `.blume`, `dist`.\n- **CI:** grep for `nuxt generate`, `nuxi`, `.output/public`, and `--extends docus`.\n\n## Docus 1.x (`@nuxt-themes/docus`)\n\nWritten from `@nuxt-themes/docus@1.15.1` and `@nuxt-themes/elements@0.9.5`. Everything above applies (run the codemod for fences, frontmatter, and the H1), with these differences:\n\n- **Config** is `app.config.ts` → `docus: { … }`:\n  - `title`, `description` → same keys\n  - `url` → `deployment.site`\n  - `socials` → `footer.socials`. Values are handles (`twitter: '@nuxt_js'`, `github: 'owner/repo'`), so build each URL.\n  - `github: { owner, repo, branch, dir }` → `github`\n  - `header.logo`/`title` → `logo`/`title`\n  - `aside.collapsed` → `navigation.sidebar.display: \"group\"`\n  - `footer.textLinks`/`iconLinks` → `footer.links`\n  - everything else → drop (report)\n- **Folders** use `_dir.yml` (`title`, `icon`, `navigation: false`, `navigation.redirect` → a redirect from the folder's route).\n- **Titles** are the body `# H1` (the codemod moves it).\n- **Frontmatter:**\n  - `layout: page` → `mode: custom` or `mode: center`\n  - `aside: false` → `mode: center`\n  - `toc: false` → `mode: wide`\n  - `redirect` → a redirect\n  - `bottom`, `header`, `fluid`, `main` → drop\n- **Components** (Nuxt Elements, left as written and reported):\n  - `::alert{type}` → that directive (`primary` → `tip`)\n  - `::callout{type}` with `#summary`/`#content` → the directive holding the summary, then `<Expandable title=\"Details\">` around the content\n  - `::list{type}` → a plain list\n  - `::badge{type}` → `<Badge>`\n  - `::button-link{href}` → a link\n  - `::terminal{content}` → a `bash` fence\n  - `::card-grid` + `::card{icon}` with `#title`/`#description` → `<CardGroup>` + `<Card>`\n  - `::block-hero` → the landing page\n  - `::code-block{label}` → a titled fence in `<CodeGroup>`\n  - `::video-player{src}` → `<video>`\n  - `::sandbox`, `::props`, `::copy-button`, `:ellipsis` → drop (report)\n- **URLs:** Nuxt Content 2 routes the same way, so check the old sitemap for trailing slashes and pin heading ids as above.\n\n## Dropped — report these\n\nReport each of these, and anything else you drop:\n\n- **Theme:**\n  - the brand color, when it was Docus's default emerald\n  - Nuxt UI overrides and theme CSS that doesn't map\n  - a custom `seo.titleTemplate`, `seo.schema`, and the TOC title\n  - logo extras, and socials with no Blume platform\n  - a forced color mode (the toggle stays)\n  - `navigation.sub: 'aside'` anchors\n  - a header button that showed only on the landing page (now site-wide)\n- **Navigation:** text-order sorting (`10.` before `2.`), and pages that sorted after a folder.\n- **Frontmatter:**\n  - `links` (now `related` cards)\n  - `sitemap: false`, `toc`, `layout`, and other unknown keys\n  - derived descriptions\n- **Components:**\n  - callout `to` links, callout colors (mapped to the nearest type; `<Callout color>` takes a CSS color when a tint matters), and card colors\n  - prompt icons and Windsurf/Claude actions\n  - the `code-tree` file browser and `code-preview` live rendering\n  - Nuxt UI landing decorations\n  - custom and module components\n  - brand icons with no SVG\n- **Server features:**\n  - the site's own MCP tools and prompts\n  - server routes and `@vercel/speed-insights`\n  - the assistant, if not kept\n  - old `/.well-known/skills/` file URLs, and pages whose prose describes any of these\n- **Anchors:** old ids that were already broken.\n",".agents/skills/blume-migrate/references/docusaurus.md":"# Docusaurus → Blume\n\nDocusaurus is a React site; navigation comes from `sidebars.js` (explicit or autogenerated) plus per-folder `_category_.json`. Admonitions are already directive syntax, which eases the content transform — **but note the source major version**: v2 (MDX v1) and v3 (MDX v3) have different content hazards, called out below.\n\n## Detect\n\n- **`docusaurus.config.{js,ts}`** at the root; a **`sidebars.{js,ts}`** file and per-folder **`_category_.json`** files.\n- Content under **`docs/`** (often plus `blog/`, `src/pages/`, `static/`, `versioned_docs/`); `@docusaurus/*` deps.\n- v2 vs v3: check the `@docusaurus/core` major in `package.json`.\n\n## Config: `docusaurus.config` → `blume.config.ts`\n\nRead `themeConfig`, `presets`, and `plugins`:\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` | `title` |\n| `tagline` | `description` |\n| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |\n| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |\n| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead (it renders in the footer) |\n| `themeConfig.navbar.items` (a `className`-styled button — \"Get started\", \"Sign up\") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |\n| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `\"system\"`) |\n| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |\n| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |\n| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md — GitHub Pages, a common Docusaurus host, is not one of them); `baseUrl` (when not `/`) → `deployment: { base: \"/…\" }`, or the `base` option of a host adapter (`vercel({ base })`) when the site also needs one |\n| preset `docs.routeBasePath` — **including the default!** | Docusaurus serves docs at **`/docs/…` by default**; the \"map only declared fields\" rule does **not** apply here because the _URLs_ are load-bearing. Either keep them with top-level **`basePath: \"/docs\"`** (invisible to the sidebar), or intentionally move to root and emit a `redirects` entry per page. Decide explicitly and say which. (`routeBasePath: '/'` = docs-only mode — nothing to do.) |\n| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`; **an origin other than `https://github.com` → `github.host`** — a GitHub Enterprise repo's edit links and header mark point at the public site without it) |\n| `themeConfig.footer` | `footer.links`, one row of `{ label, href }`: every column's items flatten into it in order (an item's `to`, a route, is its `href`); `copyright` (HTML) → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), keep a `${new Date().getFullYear()}` template literal as written, and drop a \"Built with Docusaurus.\" credit; a link inside it loses its URL (report, or add it to `footer.links`); a translated one in `i18n/<locale>/docusaurus-theme-classic/footer.json` → a locale map; column `title`s, `logo`, and `style` drop (report), or keep the whole footer as a Footer override (`defineComponents` layout slot) |\n| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |\n| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |\n| `themeConfig.algolia` | drop — Blume ships built-in search (Orama); remove the Algolia dep. To keep the index, `search: algolia({ appId, apiKey, indexName })` from `blume/search` (the DocSearch `apiKey` is the search-only key) |\n| `@docusaurus/plugin-client-redirects` | **static `redirects: [{from, to}]` arrays convert 1:1** to Blume `redirects` (a `from` array = one entry per item); only `createRedirects` functions are truly dynamic → host rules |\n| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |\n| OpenAPI doc plugins (`docusaurus-plugin-openapi-docs`, `redocusaurus`) | delete the plugin **and its generated pages** — add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list; `redocusaurus` `specs[].route` → the adapter's `route`, one `openapi()` entry (or source) per spec (see SKILL.md \"OpenAPI\") |\n| GraphQL doc generators (`@graphql-markdown/docusaurus`, `@edno/docusaurus2-graphql-doc-generator`) | delete the plugin **and its generated pages** — add `graphql({ spec, endpoint })` (from `blume/reference`) to the top-level `reference` list instead (see SKILL.md \"GraphQL\") |\n| `remark-math` + `rehype-katex` | delete — `$$…$$` renders in `.mdx` with no config (no `markdown.math` field exists), as a block or inline within a sentence; a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence |\n| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |\n\n## Static assets — required move\n\nEvery Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png` → `/img/foo.png`). Blume serves **`public/`**. **Move `static/*` (and any extra `staticDirectories`) into `public/`** — all root-relative references then keep resolving unchanged. Without this, every image and download 404s.\n\n## Navigation: `sidebars.js` + `_category_.json`\n\n- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.\n- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: \"group\"` (or `navigation.sidebar.display: \"group\"` once in config when every category collapses).\n- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder (its `title`/`description` from `link.title`/`link.description`) plus `directory: \"card\"` in the folder's `meta.ts`, which lists the folder's pages below it as the generated index did — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible: false` → that folder's `meta.ts` `display: \"flat\"`, so it stays a plain heading even under a global `\"group\"` mode. `className`/`customProps` → drop (report).\n- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.\n\n## Versioned docs\n\nDocusaurus `versioned_docs/version-X/` + `versions.json` (+ `versioned_sidebars/`) → **recommend migrating the latest released version only**. Mind the URL scheme: by default the **latest release** serves at `/docs/` and the work-in-progress `docs/` folder serves at `/docs/next` (`lastVersion: 'current'` flips this) — pick the folder that matches what users see at `/docs/`. If older versions must stay, use Blume's native versioning rather than a hand-built `navigation.selectors` dropdown: move each `versioned_docs/version-X/` into a top-level folder under `content.root` named for its id, and list that id in `versions.archived` (newest first), with `versions.current` labeling the live tree. Ids must start with a letter, so `version-1.0/` becomes `v1.0/`. Blume then adds the version switcher, the old-version notice, version-scoped search, and canonicals to the latest itself. A version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as ordinary current content. Snapshot routes become `/<id>/…`, so rewrite root-absolute links inside each snapshot to stay in it (`/guides/x` → `/v1.0/guides/x`) and add `redirects` from the old version URLs. Full reference: `docs/content/versioning.mdx` in the installed package.\n\n## Blog\n\nA Docusaurus `blog/` → Blume `type: blog` pages. **Dates come from filenames/folders** (`2024-01-31-foo.md`) — extract each into `date` frontmatter and strip the date from the filename (the old dated URLs `/blog/2024/01/31/foo` need `redirects`). Strip `<!-- truncate -->` / `{/* truncate */}` markers. `authors.yml` refs → inline author objects in each post's `authors` frontmatter. RSS stays at `/blog/rss.xml` on both sides. Blume generates **no** blog index, tag, author, or archive pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag, author, and archive pages as dropped.\n\n## Content & components\n\n- **`.md` vs `.mdx` — both majors need renames, for opposite reasons.** Blume parses `.md` as plain Markdown: no directives, no JSX, no `$$` math, no mermaid/package-install fences. **v3** treats `.md` as MDX (so a `.md` with imports/JSX/`{}` renders them as literal text in Blume); **v2** content is looser MDX v1. Rule: **rename any `.md` that contains admonitions, JSX, imports, or math to `.mdx`** — for typical Docusaurus repos that is most files.\n- **Admonitions are directives — but check the version.** v3: `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger` pass through; `:::caution` → `:::warning` (or rely on Blume's alias); titles `:::note[Title]` work. **v2:** titles are space-separated (`:::note Your Title`) — rewrite to brackets or the title is lost (`BLUME_DIRECTIVE_OPENING_TEXT` names each one); and v2's `:::warning` rendered **red/danger** — audit whether it should become `:::danger`.\n- **Tabs:** `<Tabs>`/`<TabItem label=\"…\" value=\"…\">` → `<Tabs>`/`<Tab title=\"…\">`. Drop `groupId`/`queryString`/`value`; strip the `@theme/Tabs` imports.\n- **Theme JSX in content:** `<DocCardList/>` (standard on category index pages) → delete it and set `directory: \"card\"` in that folder's `meta.ts`: Blume then lists the folder's pages as cards below the `index` page's content and keeps the list current (nested folders inherit the setting; give one `directory: \"none\"` to opt out). A hand-written `CardGroup` is only for a curated subset; `<TOCInline/>` → drop (report); `<CodeBlock>` JSX → a fenced code block; `<Admonition>` → the matching directive; `<details>`/`<summary>` → `<Accordion>`/`<AccordionItem>` or leave as raw HTML.\n- **`@theme/*` / `@site/*` imports** — strip `@theme/*` (Blume injects components globally); rewrite `@site/` asset/module paths to `/public` URLs or inline. **MDX partials** (`_partial.mdx` imports) → inline the partial's body (Blume's default `**/_*` exclude already hides the partial files themselves).\n- **Code blocks:** `title=\"file.js\"` → works as-is; `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left); **magic comments** (`// highlight-next-line`, `highlight-start`/`end`) → `{ranges}` or `// [!code highlight]` — unconverted they ship as literal comments in every sample; ` ```bash npm2yarn ` → ` ```package-install `.\n- **MDX v1 (v2 sources) pitfalls:** unescaped `<`/`{` in prose, HTML comments `<!-- -->` (→ `{/* */}`), string `style=\"…\"` attributes (→ objects). Fix as build errors surface.\n\n## Frontmatter\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` / `description` | pass through |\n| `id` | **not a safe drop** — Docusaurus builds a doc's URL from its ID, so `id: part1` in `guides/hello.md` serves `/docs/guides/part1`. Where the `id` differs from the filename (and no `slug` is set), replace it with `slug: guides/part1` (the folder path plus the id) or rename the file to match; otherwise drop it. Sidebar and `link.type: 'doc'` references name docs by this ID too |\n| `slug` | `slug` as a **full path from the content root**: Blume's `slug` replaces the page's whole route, while Docusaurus resolves a relative slug (no leading `/`) against the doc's folder. So `guides/intro.md` with `slug: start` → `slug: guides/start`; an absolute slug (`/start`) is already a full path |\n| `sidebar_label` | `sidebar.label` |\n| `sidebar_position` | `sidebar.order` |\n| `unlisted` | `hidden: true` + `noindex: true` |\n| `last_update.date` | `lastModified` |\n| `draft` | `draft` |\n| `tags` | `search.tags` (or drop if only used for the blog tag index) |\n| `image` | `seo.image` |\n| `keywords` | drop → use `seo` |\n| `sidebar_class_name` / `sidebar_custom_props` / `pagination_next` / `pagination_prev` / `pagination_label` / `hide_title` / `hide_table_of_contents` / `toc_min_heading_level` / `toc_max_heading_level` / `displayed_sidebar` / `custom_edit_url` | drop (report) |\n\nRemove any duplicated H1 in the body — `title` renders the H1.\n\n## Icons\n\nDocusaurus has no built-in icon-name system (icons are usually inline SVG/emoji/CSS). Where you add sidebar/tab icons, use Lucide names. Convert any FontAwesome/react-icons usage in content to Lucide `<Icon>` or drop it.\n\n## Teardown\n\nRemove `@docusaurus/*` and Algolia deps; delete `docusaurus.config.*`, `sidebars.*`, `babel.config.js`, and `src/` theme customizations (after the `src/pages/` inventory above; re-implement what matters via `defineComponents` layout slots or `blume eject`). Repoint `start`/`build`/`serve` scripts → `blume dev`/`blume build`/`blume preview`; add `blume`.\n\n## Dropped — report these\n\nCustom/swizzled theme components (layout slots or `blume eject`), the footer's logo and HTML items (and any link inside its copyright), the blog's generated tag, author, and archive pages, Algolia config, `sidebar_custom_props` and the other dropped frontmatter keys, `createRedirects` functions (→ host rules), React pages under `src/pages/`, `<TOCInline>`, per-category `className`/`customProps`, and any `@theme/*` component with no Blume equivalent.\n",".agents/skills/blume-migrate/references/fern.md":"# Fern Docs → Blume\n\nFern Docs is a hosted platform driven by a `fern/` folder: `docs.yml` declares the **entire** navigation (tabs, sections, pages, the API Reference, changelogs, versions, products), pages are MDX files anywhere under `fern/`, and the API Reference is generated from an OpenAPI/AsyncAPI spec or a **Fern Definition**. Two things set it apart from the other sources:\n\n- **The same `fern/` folder usually drives SDK generation too** (`generators.yml`, `definition/` or `apis/<name>/`). Only the docs move. Never delete or rewrite the API definition, a `generators.yml`, or `fern.config.json` (see [Teardown](#teardown-keep-sdk-generation)).\n- **URLs come from `docs.yml`, not from file paths.** A page's URL joins slugs from its tab, sections, and page name, and a frontmatter `slug` replaces most of that. Files sit wherever the team put them (`pages/get-started/welcome.mdx` can serve `/welcome`).\n\nScope: checked against Fern CLI 5.44.8 and an end-to-end migration of agentmail-to/agentmail-docs (115 pages, a Fern Definition with 149 endpoints and 8 webhooks, 270 old URLs). Versions, products, tab variants, translations, `folder:` entries, and OpenAPI (rather than Fern Definition) inputs weren't in that run: the guidance for them below is untested.\n\n## Detect\n\n- **`fern/docs.yml` beside `fern/fern.config.json`** (run from the repo root), or `docs.yml` + `fern.config.json` in the working directory (run from inside `fern/`). Monorepos nest it (`docs/fern/docs.yml`).\n- `fern.config.json` **without** a `docs.yml` is an SDK-only Fern project: the docs live elsewhere (often a Mintlify `docs.json`), so use that source's reference and leave `fern/` alone.\n- A stale `fern/docs.yml` can outlive a move off Fern. If the repo also carries another docs config, check which one the live site runs: Fern pages answer with an `x-fern-renderer` header (`curl -sI https://<docs-domain>/ | grep -i x-fern`).\n\n## Run the codemod first\n\nThe bundled codemod does the mechanical half: URL computation and file placement, `meta.ts`, frontmatter, the common components, assets, and every redirect. Steps, in order:\n\n1. **Save the old URL list.** Fetch `https://<docs-domain>/sitemap.xml` and keep only the paths, one per line, in `old-urls.txt` (a Fern sitemap can name a different host than the one you fetched). Hidden pages aren't in it; the codemod computes those from `docs.yml`. Run the CORS probe under [Try it](#layout-summary-snippets-try-it) while you're there.\n2. **Dry run**, from the folder that holds `fern/` (where `blume.config.ts` goes):\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt\n   ```\n\n   It prints a sitemap diff (`0 computed URL(s) not in it, 0 of its URLs not computed` is the goal; otherwise fix those pages by hand with [Fern's URL rules](#ferns-url-rules) and report them), the `navigation.tabs` entries for the config, which APIs are Fern Definitions, a TODO list of judgment calls, and the icons it couldn't map. `--out` sets the content root (default `docs`), `--public` the public folder, `--fern` the Fern folder. It **stops** on `versions` or `products`: migrate those by hand.\n\n3. **Apply** with `--write`. It moves every published page to a path that reproduces its old URL, writes frontmatter and `meta.ts` files, converts components, moves the assets pages use (and the logo and favicon) into `public/`, and saves `fern-migration.json`. A YAML or JSON file a page links to (a spec offered for download) is copied, not moved, since SDK generation may read it. It's idempotent: a rerun skips moved pages.\n4. **Write `blume.config.ts`** from [Config](#config-docsyml--blumeconfigts), with the printed tabs and `logo.href` set to the page `/` now redirects to.\n5. **Set up the API reference** ([API reference](#api-reference)): export a Fern Definition, write the overlay (tag names, webhooks, idempotency headers), add `openapi()`.\n6. **Build, then list the operation routes**:\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist > routes.json\n   npx fern-api@<version> export --api <api-name> spec.json   # JSON because the path ends in .json\n   ```\n\n   The join needs a JSON copy of the spec the build rendered: for a Fern Definition, that export (a scratch file, not the committed spec); for a YAML OpenAPI input, `npx @redocly/cli bundle <spec>.yaml --output spec.json`.\n\n7. **Join endpoints and write the redirects**:\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt          # dry run\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt --write\n   ```\n\n   One `--spec <route>=<file>` per API. It maps every old endpoint, webhook, and group URL to its Blume route, translates `docs.yml` redirects, adds the content-move, changelog, feed, and `/` redirects, rewrites content links that point at old URLs, and checks every old URL is served or redirected (it must print `every old URL is served or redirected`). `--write` saves `fern-redirects.json`; import it:\n\n   ```ts\n   import fernRedirects from \"./fern-redirects.json\" with { type: \"json\" };\n   // in defineConfig:\n   redirects: [...fernRedirects],\n   ```\n\n   Fix a redirect it flagged by filtering that entry out and adding your own after the spread.\n\n8. **Work the TODO list**, then `blume build`, `blume validate --strict`, `blume audit --only redirects`, SKILL.md's heading-anchor check (`pin-heading-ids.mjs`), and `fern check` after the [teardown](#teardown-keep-sdk-generation).\n\nWhat it leaves to you: the config, the overlay, the TODO list, the components it names, links already broken on Fern, CI, and contributor docs. It never touches `generators.yml`, the definition, or a spec.\n\n## Inventory\n\nRead `docs.yml` first, then every file it points at: `redirects` (a list, or YAML files that each hold a `redirects:` list), `tabs.<key>.changelog`, every `path:` in `navigation`, `css`, `js`. **Every path in a Fern YAML file is relative to that YAML file.**\n\n- **Unpublished drafts.** Fern builds only the files `docs.yml` references plus each changelog folder; any other `.md`/`.mdx` under `fern/` is a draft nobody sees. The codemod leaves them in place and lists them. Blume would publish them: delete them or move them to a `_`-prefixed folder (`_drafts/`), and report the list.\n- **SDK generation.** Every `generators.yml` and its `groups` stay exactly as they are.\n- **Second instances.** Each `instances[]` entry is a deployment of the same docs (a second domain, a subpath). One Blume build serves one; report the rest (see [Deploy](#urls-redirects-and-deploy)).\n\n**Where the Blume project goes.** If the repo root has a `package.json`, put `blume.config.ts` there with content in `docs/` (the default root). If it has no JavaScript toolchain (a Python or Go repo whose docs were only `fern/`), make a self-contained docs package (`docs/` holding `package.json`, `blume.config.ts`, and `content.root: \"content\"`), run the codemod from there with `--fern ../fern --out content`, and say which you chose. Don't make `fern/` itself the content root: it keeps holding the SDK config. See `references/monorepo.md` for workspaces.\n\n## Config: `docs.yml` → `blume.config.ts`\n\nMap only what's set.\n\n| Fern `docs.yml` | Blume | Notes |\n| --- | --- | --- |\n| `title` | `title` | Fern shows it only in the browser tab; Blume also shows it beside the logo (see `logo`) |\n| `logo` (`{ light, dark, href, height, right-text }`) | `logo: { image: { light, dark, alt }, href, text }` | the codemod moves the files into `public/`. Fern shows the image alone unless `right-text` is set, so set **`text: right-text`, or `text: \"\"`**. A local monochrome SVG can collapse to `logo: \"/logo.svg\"` with `currentColor` fills (SKILL.md). Set `href` to the first page when nothing serves `/`. `height` drops |\n| `favicon` | **drop the field** | the codemod moves the file to `public/favicon.<ext>`; Blume detects it by filename |\n| `colors.accent-primary` (or `accentPrimary`), a string or `{ light, dark }` | `theme.accent` | same shape |\n| `colors.background` | `theme.background` | same shape |\n| `colors.*`, `layout.*`, `settings.*` (others), `theme.*`, `global-theme`, `page-actions`, `experimental`, `check`, `ai-examples`, `libraries`, `roles` | **drop** (report) | `global-theme` merged a registry theme at publish: read its values off the live site |\n| `typography.bodyFont` / `headingsFont` / `codeFont` | `theme.fonts.body` / `display` / `mono` | Fern fonts are files in the repo (`path`, or `paths` with `weight`/`style`): each role takes `{ name, variants: [{ src, weight, style }] }` |\n| `navbar-links` `type: filled` / `primary` | `navigation.cta` (`{ label: text, href }`) | one only; extra filled links go to `actions`. `url` is Fern's old name for `href` |\n| `navbar-links` `type: minimal` / `outlined` / `secondary` | `navigation.actions` (`[{ label: text, href }]`) | text only: link icons drop. A `mailto:` link works |\n| `navbar-links` `type: github` / `dropdown` | `github: { owner, repo }` / **no equivalent** | flatten a dropdown into `actions` (report) |\n| `footer-links` | `footer.socials` | keys carry over except `twitter` → `x`, `hackernews` → `hacker-news` |\n| `footer` / `header` (React files) | **drop** (report) | or a layout slot in `components.ts` |\n| `instances[].custom-domain` | `deployment.site` (`https://…`) | only when the target host isn't Vercel or Netlify (SKILL.md). **A domain with a path** (`acme.com/docs`) is a reverse-proxied subpath → `deployment: { base: \"/docs\" }`. **Several instances** → one Blume build; keep the canonical one (`metadata.canonical-host` names it when set) and report the rest |\n| `instances[].edit-this-page.github` (`{ owner, repo, branch }`) | `github: { owner, repo, branch, dir }` | `dir` = path from the repo root to the folder holding `blume.config.ts` |\n| `metadata.canonical-host` | `deployment.site` | same host rule as `custom-domain`; a path in it is the `base` above |\n| `metadata.og:description` / `twitter:site` | `description` / `seo.x.handle` | other `og:*`/`twitter:*` drop: Blume writes them |\n| `announcement.message` | `banner` | a string, or `{ content, link: { text, href } }` |\n| `analytics.*` | the matching factory from `blume/analytics` (`posthog`, `googleAnalytics`, `googleTagManager`, `segment`), or `script()` | a `${VAR}` value → `process.env.VAR` |\n| `js` (a path, `{ path, strategy }`, or `{ url, strategy }`) | `script({ src \\| content, strategy })` per tracking script | a third-party loader ports; code that reads or restyles Fern's DOM (`.fern-*`) doesn't (report it). Fern's strategies (`beforeInteractive`, `afterInteractive`, `lazyOnload`) aren't Blume's (`async`, `defer`): pick one or leave it out. A `<link rel=\"help\" href=\"/llms.txt\">` injection drops: Blume publishes `llms.txt` |\n| `css` | a root `theme.css` | `.fern-*` selectors match nothing in Blume; report what you drop |\n| `default-language`, `settings.http-snippets` | `codeSamples` on `openapi()` | that language first; a list → that list; `false` → `false` |\n| `settings.disable-search` | `search: false` |  |\n| `layout.hide-feedback` | `feedback: false` |  |\n| `ai-search` / `ai-chat` (Ask Fern) | **drop** (report) | Blume's assistant is opt-in and self-hosted (`ai.assistant`, a provider key, server output); offer it, don't enable it silently |\n| `agents.*` | **drop**, or `agents.llmsTxt.details` for directive text | Blume generates `llms.txt` and `robots.txt` |\n\n## Navigation\n\n### Fern's URL rules\n\nThe codemod applies these; you need them for its sitemap diff and anything it doesn't handle. Each level contributes a slug, joined outermost first: **product → version → tab → sections → page**.\n\n- **Each slug is the item's `slug:` if set, else lodash `kebabCase` of its display name**: tab `display-name` (not the tab's key), `section:` title, `page:` name. `kebabCase` lowercases, drops punctuation, and splits camelCase and letter/digit boundaries: `API Reference` → `api-reference`, `AgentMail` → `agent-mail`, `SOC 2` → `soc-2`, `v3 (Latest)` → `v-3-latest`.\n- **`skip-slug: true`** on a tab, section, or api entry drops that level from the URL but keeps it in the sidebar. A changelog tab ignores it.\n- **A frontmatter `slug` is the page's whole URL below any product/version prefix**: it replaces the tab and section slugs too. A `docs.yml` `slug:` replaces only that item's own segment.\n- **An `api` entry's base** is its `summary` page's frontmatter `slug` when set; otherwise its `slug` (or `kebabCase(title)`) under the tab.\n- **A changelog** serves its index at its slug and **each entry at `<changelog>/<YYYY>/<M>/<D>`** (no zero padding), titled with the date (`September 5, 2026`). Two entry files dated the same day share one URL: Fern merged them into one page.\n- **`/`** answers with a 307 to the first page when no landing page is set. **API group paths** (`/api-reference/inboxes/threads`) answer with a 307 to the group's first endpoint and aren't in the sitemap, but content links to them.\n- **The first version is the default and has no version segment**; every other version is prefixed with its slug. Products always carry their slug.\n\n### What the codemod builds\n\n- **One folder per tab** at the tab's URL prefix. The tab whose pages share no single prefix (a `skip-slug` tab, or one whose pages all carry root-level frontmatter slugs) becomes the content root, with a tab at `path: \"/\"`. Only one tab can be the root; the codemod reports a second one, which must move under a prefix (route change, redirects).\n- **A section** whose pages share a URL segment → a real folder; one that adds no segment → a `(group)` folder. A section's `path` (its overview page) → that folder's `index.mdx`.\n- **A page whose URL departs from its folder** (a frontmatter `slug`) stays in its sidebar folder with `slug` set, since Blume places a page in the sidebar by file path, not route.\n- **A file `docs.yml` lists twice** is published once, at its first listing; the other URL redirects there (reported).\n- **`meta.ts`** per folder: `pages` in `docs.yml` order, the section title and Lucide icon, and a collapsible section (`collapsed`, `collapsible`, `collapsed-by-default`) → `display: \"group\"`. Blume lists loose pages above subgroups, so a section that interleaves pages and subsections renders its pages first: report any such reorder.\n- **Labels.** Fern labels a page in the sidebar with its `page:` name (or `sidebar-title`) and titles it with frontmatter `title`. So: no `title` → `title: <page name>` (or an H1 that opens the body, which it removes); a different `title` → `sidebar.label: <page name>`. The api **summary** page keeps its `title` with no label (its page name usually repeats the tab label).\n- **`hidden: true`** → `hidden: true`, `seo: { noindex: true }`, and `search: { exclude: true }`: Fern keeps hidden pages reachable but out of search and indexing.\n- **The root URL.** Nothing serves `/` under a root tab, so it redirects `/` to the first page (307), as Fern did; set `logo.href` to that page. The tab at `/` links to the sidebar's first page on its own. A root `index.mdx` would move the landing page to `/`; do that only if the user wants `/` as the landing URL.\n\nBy hand: `availability` (`beta`, `deprecated`, …) → `sidebar.badge` (`Beta`) or `deprecated: true`; `- link:` items → `navigation.featured` (`{ label, href, icon? }`) or an explicit `navigation.sidebar` link (report which); a hidden **tab** (its printed line ends `(hidden on Fern)`) → leave it out of `navigation.tabs`; the codemod already marked its pages hidden.\n\n### Tabs\n\nThe codemod prints one `navigation.tabs` entry per tab (`{ label: display-name, path, icon }`). Keep them as header tabs.\n\n- An **`href` tab** (Blog, GitHub, a dashboard) has no content → `navigation.actions` or `navigation.featured`; a GitHub tab → `github`.\n- **Tab `variants`** (several sidebars under one tab): the codemod reports them. One subfolder per variant and a tab with `items` (`{ label, path }` per variant); variant pages move, so add redirects and report the approximation.\n\n### Changelog\n\nThe codemod moves each entry to `changelog/<YYYY-MM-DD>[-suffix].mdx` (`MM-DD-YYYY` and `MM-DD-YY` names are renamed, so entries sort by date and a two-digit year's month isn't read as an ordering prefix. Fern parsed those in the publishing machine's time zone, so one can sit a day off: the sitemap diff shows it) with `type: changelog`, `date`, and a `title`: the frontmatter one, the date as Fern showed it, or the file suffix sentence-cased (`agentid-sign-in-keys` → \"Agentid sign in keys\"). **Review every suffix title** (`AgentID sign-in keys`); the TODO list names them. `tags` → `changelog.category` (the first) and `search.tags` (all).\n\n- **Same-day entries.** The day's Fern URL redirects to the unsuffixed entry, or else the first by file name. Fern showed both on one page: consider linking the second from the first, and report it.\n- `overview.mdx` (or `index.mdx`, `summary.mdx`) → its text goes into `changelog: { title, description }` in `blume.config.ts`, since an `index.mdx` would replace Blume's generated `/changelog` index (a list, where Fern's index rendered whole entries). Its `layout` and `authors` drop.\n- A non-date file (`TEMPLATE.mdx`) was never published: move it out or `_`-prefix it, and update it to Blume's format if contributors copy it.\n- The feeds `/<changelog>.rss` (`.atom`, `.json`) redirect to `/changelog/rss.xml`, which needs a site URL.\n- For a public GitHub repo, offer `githubReleases()` as SKILL.md \"Changelogs\" says; keep the files when the user says the entries are curated rather than release notes.\n\n### Versions and products (by hand, untested)\n\n- **`versions`** → Blume's native versioning (`docs/content/versioning.mdx` in the installed package). The first Fern version is the live tree at the content root; every other version → a folder named by its slug, listed in `versions.archived`. Place each version file's `navigation` with the same rules; a `ref:` version lives in git, so check it out. Per-version API references → one `openapi()` each, with its own `route`.\n- **`products`** → one folder per product plus `navigation.selectors: [{ kind: \"product\", label, items }]`.\n- `<If versions products>` → keep each folder's matching branch.\n\n## Content and components\n\nThe codemod writes every page as `.mdx` and converts:\n\n- **Callouts.** `<Note>`, `<Tip>`, `<Info>`, `<Warning>`, `<Success>`/`<Check>`, `<Error>`, `<Launch>`, and `<Callout intent>` (no intent → `info`, as Fern rendered it). A top-level one becomes a `:::type[Title]` directive (dedented). One whose icon maps to Lucide (a directive can't set one), or one nested in a component, becomes `<Callout type title icon>`; an icon with no Lucide equivalent drops (reported).\n- **Code groups.** `<CodeBlocks>` → `<CodeGroup>`; every untitled fence inside a group gets Fern's language title (`title=\"Python\"`, `title=\"cURL\"`). Blume labels an untitled grouped fence by its language anyway, so the title only keeps Fern's spelling. `<CodeBlock title>` wrappers are unwrapped onto the fence: Blume's `<CodeBlock>` renders a wrapped fence, but not the wrapper's `title`.\n- **Fence metas.** Multi-word bare titles are quoted (Fern and Blume both read every word of ` ```js Snippet with title` as the title), `filename=` → `title=` and `wordWrap` → `wrap` (Blume warns `BLUME_CODE_FENCE_OPTION` on the Fern spellings), `env` → `dotenv` (Shiki has no `env`: `BLUME_UNKNOWN_CODE_LANGUAGE`).\n- **Cards and accordions.** `<Cards>` → `<CardGroup>`, card icons → Lucide, Fern-only card props drop. `<AccordionGroup>`/`<Accordion>` → `<Accordion>`/`<AccordionItem>`; a lone `<Accordion>` is wrapped in one.\n- **`<Icon>`**: icon → Lucide, `size` × 4 (Fern counts in 4px units). **`<Frame>`**: `background` drops. **`<Tab language>`**: `language` drops (Blume syncs same-titled tabs).\n- **`<ParamField path>`** → `<ParamField name>`: Fern's `path` is the field's name, and Blume's `path=` would mark it a path parameter. `toc` drops. Nested fields in an `<Indent>` (reported) → an `<Expandable title=\"properties\">` in the parent field's body.\n- **Snippets.** `<Markdown src=\"/snippets/x.mdx\" planName=\"x\" />` → `<include planName=\"x\">/_snippets/x.mdx</include>` on a line of its own, with the snippet moved to `<content-root>/_snippets/`. Its `{{planName}}` placeholders carry over as written; Blume reads an include prop only when its name starts with a lowercase letter, so rename any that don't.\n- **Embeds and comments.** A YouTube iframe → `<YouTube id>` (a playlist embed → `<YouTube url>`); `allowfullscreen`/`frameborder` → `allowFullScreen`/`frameBorder` (MDX drops the lowercase forms silently); `<!-- -->` → `{/* */}`.\n- **Assets.** `/assets/x.png` and `../x.png` → moved into `public/` at their path from `fern/` and linked root-absolute. A YAML or JSON file is copied instead (SDK generation may read it; the copy goes stale when it changes). An asset no page or config references stays put: move or delete it.\n- **Links.** Absolute links to the docs' own domain → root-relative; links to moved pages, endpoints, and Fern's `api:METHOD/path` syntax → the new routes (the `endpoints` step).\n\n**Reported for you to convert** (a leftover Fern tag gets a `BLUME_UNKNOWN_COMPONENT` warning, or crashes the page):\n\n| Fern | Blume |\n| --- | --- |\n| `<EndpointRequestSnippet endpoint=\"POST /x\">`, `<EndpointResponseSnippet>` | a hand-written fenced sample plus a link to the operation page: Blume can't embed an operation's sample in prose |\n| `<EndpointSchemaSnippet>`, `<Schema>`, `<SchemaSnippet>`, `<WebhookPayloadSnippet>`, `<RunnableEndpoint>` | a link to the operation page; a small schema can become a `TypeTable` |\n| `<Badge intent outlined>`, `<Availability type>` | `<Badge variant stroke>` (`success`, `warning`, `error` → `danger`, `tip` → `accent`); an availability that labels the page → `sidebar.badge` |\n| `<Code src title>` | `<include meta='title=\"…\"' lang=\"…\">./path</include>` with the file inside the content root |\n| `<Files>`/`<Folder>`/`<File>` | `<Tree>`/`<Tree.Folder>`/`<Tree.File>` |\n| `<Versions>`/`<Version title>` | `<Tabs>`/`<Tab title>` |\n| `<If>` (roles), `<Feature flag>` | the public or default content; ask before dropping role-gated text |\n| `<Button href>`, `<Download src>`, `<Copy>` | a link (or `<Card href>`), a link to the file in `public/`, inline code |\n| `<Aside>`, `<Indent>`, `<StickyTable>`/`<SearchableTable>`/`<PaginatedTable>` | the content inside |\n| `<Prompt title>`, `<ScrollWalkthrough>` | `<Prompt description>`; `<Steps>` with a code block per step |\n| `<ChangelogTags>`, `<Template>` | delete (report) |\n\nNot reported, but check: headings inside `<Steps>` (Fern makes each a step) → one `<Step title>` each; `<Anchor id>` → `<a id=\"x\"></a>`; `<llms-only>`/`<llms-ignore>` → `<Visibility for=\"agents\">`/`<Visibility for=\"web\">`; `maxLines` → `expandable`; inline `$…$` math → `$$…$$`; custom React components → an island or a `components.ts` override. **Unclosed fences** (Fern rendered an empty block): the codemod closes them at the end of the file and reports them; delete the stray fence.\n\n### Frontmatter\n\nFern ignores unknown keys; Blume's schema is strict. The codemod maps `title`, `sidebar-title` → `sidebar.label`, `subtitle` → `description` (Blume renders `description` under the title), `description` → `seo.description` when `subtitle` took `description`, `headline` → `seo.title`, `slug`, `noindex` → `seo.noindex`, and the `docs.yml` `icon`. It reports every other key; map these by hand:\n\n| Fern | Blume |\n| --- | --- |\n| `canonical-url` | `seo.canonical` (absolute) |\n| `image`, `og:image` | `seo.image` |\n| `keywords` | `search.keywords` (a list) |\n| `last-updated` | drop it everywhere and set `lastModified: \"git\"` site-wide; retire any workflow that stamps it, since the key now fails the build |\n| `layout` | `mode`: `overview`/`reference` → `wide`, `page` → `center`, `custom` → `custom`, `guide` → drop |\n| `hide-nav-links: true` | `pagination: false` |\n| `availability` | `sidebar.badge`, or `deprecated: true` |\n| `position` (orders pages in a `- folder:` entry) | `sidebar.order` |\n| `hide-toc`, `force-toc`, `max-toc-depth`, `hide-feedback`, `hide-page-actions`, `no-image-zoom`, `breadcrumb`, `search-metadata`, `edit-this-page-url`, `logo`, `nofollow`, `jsonld:breadcrumb`, `og:*`, `twitter:*`, `excerpt`, `tags` | drop (report) |\n| keys Fern never read (`sidebar_position`, `lastUpdated`) | drop |\n\n### Icons\n\nFern uses Font Awesome; Blume is Lucide-only. The codemod normalizes Fern's spellings (`fa-solid fa-bolt`, `solid bolt`, bare `bolt`), maps them to Lucide names Blume bundles, and reports the rest. Most brand icons (`discord`, `google`, `aws`, `python`, `node-js`, `x-twitter`) have no Lucide equivalent: they drop. An image path on a component (`icon=\"./icons/x.svg\"`) → `public/` and a root-absolute path; one in `docs.yml` is reported for you to move. For icons you write yourself (config, hand conversions), check each name from the folder holding `blume.config.ts`; an unknown icon renders nothing:\n\n```bash\nnode -e \"const p=require.resolve('@iconify-json/lucide/icons.json',{paths:[require('path').dirname(require.resolve('blume/package.json'))]});const j=require(p);for(const n of process.argv.slice(1))console.log(n,n in j.icons||n in (j.aliases||{}))\" zap mail-check discord\n```\n\n## API reference\n\nEach `- api:` entry → one `openapi()` (or `asyncapi()`/`graphql()`) source from `blume/reference`, with **`route` set to the api entry's old base URL** so the overview keeps its address, and a tab at that route. Find the spec each entry renders:\n\n- `specs:` on the api entry → those files (`type: openapi | asyncapi | graphql`).\n- Otherwise `api-name: <name>` → `fern/apis/<name>/`, or `fern/` with one API. There, `generators.yml` `api.specs` lists OpenAPI/AsyncAPI files (older projects use top-level `openapi:`/`async-api:`); **a `definition/` folder is a Fern Definition**, which the codemod's dry run names.\n\n### OpenAPI and AsyncAPI inputs\n\n- **Point `spec` at the file where it lives** (SDK generation reads it too).\n- **Fern `overrides`** are Fern's deep-merge format, not OpenAPI Overlays. Most hold only `x-fern-*` SDK extensions, which Blume ignores; content an override changes for readers → an Overlay in `overlays`. Fern's own `overlays:` files carry over.\n- **`x-fern-ignore: true` operations would appear** in Blume: remove them with an overlay action (`target: $.paths.*[?@['x-fern-ignore'] == true]`, `remove: true`), likewise operations an api entry's `audiences` filtered out.\n- gRPC and OpenRPC references have no Blume renderer: report them.\n\n### Fern Definition inputs → export to OpenAPI\n\nBlume can't read a Fern Definition. Export it with the Fern CLI, which runs offline with no login:\n\n```bash\nnpx fern-api@<version from fern/fern.config.json> export --api <api-name> openapi/<api-name>.yml\n```\n\n- The output is **OpenAPI 3.0.1** (Blume upgrades it). Operation ids are `<package path>_<endpoint>` (`inboxes_threads_list`); each Fern package becomes one PascalCase tag (`InboxesThreads`); `info.title` is the API's name with an empty `version`.\n- **It drops webhooks, WebSocket channels, and `idempotency-headers`.** Restore the first and last with the overlay below; a WebSocket channel has no Blume page, so redirect its old URL to the guide that covers it and report it.\n- **A committed export.** If the repo already commits an OpenAPI generated from the definition (a `fernapi/fern-openapi` generator group writing locally, or a CI step running `fern export`), point `spec` at it, then check it isn't stale: export to a scratch path and diff. If they differ, report it and refresh it with the repo's own command (`fern generate --group <group> --local`), not a second copy. Look for workflows that trigger on that file (a release pipeline) before committing a refresh. Only when nothing produces a committed file, commit your export and add a `\"docs:spec\"` script that reruns it.\n\nDocs-only changes go in an overlay so the spec stays regenerable. The agentmail overlay, abridged:\n\n```yaml openapi/docs.overlay.yaml\noverlay: 1.1.0\ninfo: { title: Docs fixes, version: 1.0.0 }\nactions:\n  - target: $.info\n    update: { title: AgentMail API }\n  # Tags are flat in Blume: name the concatenated package tags.\n  - target: $.paths.*.*.tags[?@ == 'InboxesThreads']\n    update: Inbox Threads\n  # Fern's sidebar order.\n  - target: $\n    update:\n      tags: [{ name: Inboxes }, { name: Inbox Threads }]\n  # Webhooks the export dropped: their payload schemas survive in components.\n  - target: $\n    update:\n      x-webhooks:\n        messageReceived:\n          post:\n            summary: Message Received\n            tags: [Webhook Events]\n            requestBody:\n              content:\n                application/json:\n                  schema: { $ref: \"#/components/schemas/MessageReceivedEvent\" }\n            responses:\n              \"200\":\n                description: Return a 2xx status to acknowledge the event.\n  # One action per `idempotent: true` endpoint in the definition.\n  - target: $.paths['/v0/inboxes/{inbox_id}/messages/send'].post.parameters\n    update:\n      - name: Idempotency-Key\n        in: header\n        schema: { type: string }\n```\n\n**Restore webhooks this way** rather than redirecting their pages: one `x-webhooks` entry per webhook in the definition, keyed by its name, with a `$ref` to its payload schema. Blume upgrades `x-webhooks` to `webhooks` and gives each a page at `<route>/<tag-slug>/<kebab-name>`, which the `endpoints` step matches to the old URL. Nested packages (`Inboxes › Threads`) become sibling tags: report the lost nesting.\n\n### Layout, summary, snippets, Try it\n\n- **`summary`** (a page shown at the API's root) → the codemod places it at `<route>/introduction.mdx`, a normal page in the reference sidebar. The old base URL serves Blume's overview, which renders `info.description`.\n- **`layout`**, `alphabetized`, `flattened`, `paginated` → Blume orders tag groups by the top-level `tags` list (set it in the overlay) and operations within a tag in the spec's order (paths as listed, each path's methods as written). An endpoint order that `layout` or `alphabetized` set and the spec doesn't share → a `pages` list of operation slugs in `<route>/<tag-slug>/meta.ts`, or report it. An endpoint's `title` → its `summary` via overlay; `hidden: true` endpoints → remove them or let them show (ask).\n- **`snippets`** (SDK package names) → Fern rendered SDK calls; Blume generates HTTP-client samples. Add `x-codeSamples` through the overlay to keep SDK calls, or report the loss.\n- **Try it.** Fern's API Explorer proxies requests; Blume's playground sends them from the browser, so the API must answer CORS for the docs origin. Probe it:\n\n  ```bash\n  curl -si -X OPTIONS https://<api-host>/<path> -H 'Origin: https://<docs-domain>' -H 'Access-Control-Request-Method: GET'\n  ```\n\n  No `Access-Control-Allow-Origin` in the answer → `playground: false`, and report it; the alternative is `playground: { proxy: true }`, which needs server output and a host adapter (SKILL.md). `environments: []` → `playground: false`; `oauth: true` → Blume takes a pasted token and doesn't run the flow (report).\n\n### API URLs\n\nThe `endpoints` step computes these; check its output against them.\n\n- **Fern Definition**: the operation id split on `_`, each part kebab-cased, under the base: `inboxes_threads_list` → `/api-reference/inboxes/threads/list`.\n- **OpenAPI**: with `x-fern-sdk-method-name`, `<base>/<x-fern-sdk-group-name>/<x-fern-sdk-method-name>`; otherwise `<base>/<first tag>/<the operation id minus the tag's leading words>`, or the summary when there's no operation id. Each part kebab-cased (`v1` → `v-1`). Read from Fern's parser, not from a migrated site: the `endpoints` step's coverage check catches any URL it gets wrong.\n- **Group paths** → the overview's anchor for that tag: `/api-reference/inboxes/threads` → `/api-reference#inbox-threads` (307, as Fern's was). Content linking to a group gets the same anchor; `blume validate` checks it.\n\nBlume routes rarely match Fern's (the `endpoints` step skips any that do), and a pattern redirect over the old prefix also covers the new pages (`BLUME_REDIRECT_MATCHES_PAGE`), hence one exact redirect per endpoint. Don't shorten operation ids with an overlay: OpenAPI needs them unique across the whole spec, and shortened ones repeat (`list` in every tag), which warns `BLUME_OPENAPI_DUPLICATE_OPERATION_ID`.\n\n## URLs, redirects, and deploy\n\n- **`docs.yml` redirects** → `{ from, to, status }`: Fern's `permanent` defaults to true (308), `permanent: false` → 307; Blume's own default is 301, so the codemod writes the status. `:name` and `:name*` carry over; `:name+` and regex groups are reported (expand them into exact entries). A destination that moved is followed to its final route, and a `:slug*` pattern into the reference expands into one entry per operation. Trailing-slash duplicates merge.\n- **A destination that's neither a page nor another redirect** was already broken on Fern (the TODO list names it): point it at the intended page and report it.\n- **`blume validate --strict` finds links that already 404 on Fern.** Fix the link, don't add a redirect for it, and report the list.\n- **A subpath instance** (`acme.com/docs`): sources, destinations, and links carry the subpath; strip it, since Blume adds `base` itself. Serving both a domain and a subpath means two builds.\n\n## Teardown (keep SDK generation)\n\n**Keep**, even with no docs left in it: `fern/fern.config.json`, every `generators.yml`, the Fern Definition, every spec, override, and overlay a `generators.yml` references, and CI steps that run `fern check`, `fern generate --group <sdk>`, or `fern export` (with the `FERN_TOKEN` secret if they use it).\n\n**Remove**, once migrated: `fern/docs.yml`, redirect YAML files, the docs folders that moved (pages, `changelog/`, `snippets/`, assets, docs `components/`, `css`/`js`), CI steps and wrapper scripts that publish or preview the docs (`fern generate --docs`), and the `fern-api` dependency only if nothing else uses the CLI.\n\nThen **grep the repo for the old paths and syntax**: `fern/pages`, `fern/changelog`, `fern/snippets`, `docs.yml`, `<CodeBlocks>`, `<Note>`, `last-updated`. Contributor docs (`AGENTS.md`, `CONTRIBUTING.md`, templates) and generators that write into `fern/snippets/` need the new paths, frontmatter, and components. Run `fern check` afterwards.\n\nRepoint `package.json` scripts to `blume dev`/`build`/`preview`, add `blume`, and regenerate the lockfile (SKILL.md step 6). Tell the user what can't be committed: moving the custom domain's DNS (or the proxy behind a subpath instance) off Fern, then unpublishing the site in the Fern dashboard.\n\n## Dropped: report these\n\n- **Hosted features**: the API Explorer's proxy, login-injected credentials, and OAuth flow; Ask Fern search and chat; AI-generated examples; **Fern's docs MCP server (`/_mcp/server`)**, which has no static equivalent (Blume's MCP server is at `/mcp` and needs server output; `.md` page mirrors and `llms.txt` carry over), so update content that advertises it; the dashboard, analytics, preview links, RBAC and authenticated docs (host-level protection instead, per the deployment docs' **Private docs** section), feature flags.\n- **API reference**: SDK snippets (unless rebuilt as `x-codeSamples`), WebSocket channel pages, a `layout` or `alphabetized` endpoint order you didn't rebuild with `meta.ts`, nested sections, hidden endpoints, gRPC/OpenRPC references.\n- **Navigation and theme**: dropdown and link icons, approximated variants, sidebar reorders, moved `link` items, extra `colors`, `layout`, custom `header`/`footer`, `css` aimed at `.fern-*`, and `js` that isn't a tracking script.\n- **Content**: API embeds replaced by samples or links, code tooltips, `<ChangelogTags>`, role gating, and each dropped frontmatter key.\n- **Icons** with no Lucide equivalent, by name and where they were used.\n- **Unpublished drafts** and what was done with them.\n- **Second instances** and what replaces them.\n",".agents/skills/blume-migrate/references/fumadocs.md":"# Fumadocs → Blume\n\nFumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present.\n\n## Detect\n\n- Content under **`content/docs/`** (the Fumadocs convention).\n- Per-folder **`meta.json`** files.\n- `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps, and collections declared either in a `source.config.ts` (the Config API) or, in newer scaffolds, with `defineDocs` from `fumadocs-mdx/macro` inside `lib/source.ts` (no `source.config.ts` at all).\n- A `loader({ baseUrl })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`). Newer scaffolds pass a constant (`baseUrl: docsRoute`) defined in `lib/shared.ts` — resolve it there.\n\n## Config\n\nFumadocs declares little Blume needs — read the loader and the shared layout options:\n\n- **Title** — the header title: `nav.title` in `baseOptions` (`lib/layout.shared.tsx`, or `app/layout.config.tsx` in older scaffolds; often the `appName` constant from `lib/shared.ts`; extract the text if it's JSX). Without one, read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case); for a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead.\n- **Header settings** (the same `baseOptions`): `githubUrl` → `github: { owner, repo }` (the repository and edit links; when it's built from a `gitConfig` constant in `lib/shared.ts`, `{ user, repo, branch }` → `{ owner: user, repo, branch }`); `links` entries → `navigation.actions` (`{ label: text, href: url }`), except a `type: \"icon\"` link to a platform `footer.socials` knows (Discord, X, …), which goes there instead. Menus, custom items, and JSX in `nav` → report.\n- **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: \"/docs\"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `\"/\"`, serve from the site root (nothing to set).\n\nEverything else is Blume defaults.\n\n**Also read the MDX options** — remark/rehype plugins live in `source.config.ts` `mdxOptions`, or, with the macro API, in the collection options passed to `defineDocs` (a macro project may still keep a `source.config.ts` for global options):\n\n- `remark-math`/`rehype-katex` → nothing to configure: `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field), as a block or inline within a sentence. A single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence.\n- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.\n- Custom Shiki transformers/themes or other plugins → report.\n\n## Navigation: `meta.json` → `meta.ts`\n\n**Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`):\n\n| Fumadocs `meta.json` | Blume `meta.ts` |\n| --- | --- |\n| `title` | `title` |\n| `icon` | `icon` — **convert the casing** (see Icons below) |\n| `defaultOpen: false` | `collapsed: true` |\n| `defaultOpen: true` | `collapsed: false` |\n| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |\n| `pages: [...]` slugs | `pages: [...]` (ordering) |\n| `description` | **drop** (folders have no description) |\n| `collapsible: false` | that folder's `display: \"flat\"` (a plain heading, even under a global `\"group\"` mode) |\n\n`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`, `directory` (`\"card\"`/`\"accordion\"` lists the folder's pages below its `index` page — the replacement for a hand-written card list of the folder's children). Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: \"group\"` (drill-in is `\"page\"`); when the whole sidebar should collapse, set `navigation.sidebar.display: \"group\"` once in `blume.config.ts` instead of repeating it per folder.\n\n**Numeric filename prefixes move URLs.** Fumadocs builds slugs from the file path as written, so `01-intro.mdx` serves `/docs/01-intro`; Blume reads a `01-` prefix on a file or folder as sort order and drops it from the route (`/docs/intro`). Add a `redirects` entry for every prefixed file or folder (or pin the old path with frontmatter `slug`, a full path from the content root), and write `pages` entries without the prefix (`\"intro\"`, not `\"01-intro\"`).\n\n**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.\n\nHandle the `pages` array items:\n\n- **`\"...\"`** (rest marker) / `\"\"` → drop; Blume appends unlisted pages automatically. **`\"z...a\"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.\n- **A `pages` list with no `\"...\"`** → Fumadocs shows **only** the listed items; every page and folder it leaves out is hidden from the sidebar (still served by URL). Blume appends unlisted children, so each one **resurfaces** — set `sidebar.hidden: true` in each left-out page's frontmatter (for a left-out folder, in every page under it — a folder with no visible page drops out of the sidebar).\n- **`\"!page\"`** (exclusion from `\"...\"`) → the same trap: Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead.\n- **`\"---Section---\"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead.\n- **`\"...folder\"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.\n- **`\"[Text](url)\"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`).\n- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.\n\n## Frontmatter\n\nFumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). **`full: true`** (full width, TOC folded into a popover) → `mode: wide` (full column width, no TOC — the closest match; report); **`_openapi`** marks a generated API stub — delete the whole page (see OpenAPI). Any other non-schema key is a build error, so drop and report.\n\n## Icons: lucide-react names → kebab-case\n\nFumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`\"BookOpen\"`, `\"HomeIcon\"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons).\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare `<Callout>`** → `:::info`. `title` → `:::type[Title]`. An `icon` (a JSX element there) needs a `<Callout type=\"…\" title=\"…\" icon=\"cpu\">` with its kebab-case Lucide name, since a directive can't set one; drop it if it only restated the type's icon.\n- **Cards:** `<Cards>` → `<CardGroup>`. `<Card>` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={<Cpu />}` imported from `lucide-react`) → Blume takes a string name (`icon=\"cpu\"`); Fumadocs `description=\"…\"` → Blume has no `description` prop — move the text into the card body.\n- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).\n- **File trees:** `<Files>` → **`<Tree>`** (the JSX container for `Tree.*` children — _not_ `<FileTree>`, which wraps a Markdown list); `<Folder>` → `<Tree.Folder>` (`defaultOpen` carries over); `<File>` → `<Tree.File>`. Or convert the whole block to a list-driven `<FileTree>`.\n- **Tabs:** Fumadocs declares labels on the parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value=\"npm\">`. Blume's `<Tab>` carries its own `title`. Strip `items={[…]}` from `<Tabs>` and give each child `<Tab>` a `title` (from its `value`, or the positional `items` entry).\n- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.\n- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link: { href, text }, dismissible, id }`).\n- **`<include>./partial.mdx</include>`** — Blume uses the same include syntax. **Pass through unchanged**: paths resolve relative to the including file, a leading `/` resolves from the content root, and nested includes work. Keep partials in `_`-prefixed files or folders so they stay out of routing and the sidebar.\n- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.\n- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory the MDX components file before deleting it** — `components/mdx.tsx` (`getMDXComponents`) in newer scaffolds, `mdx-components.tsx` in older ones; components registered there are used import-free in MDX bodies; port or inline each usage first.\n\n## Headings\n\nTrailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from the TOC), `[toc]` (TOC-only entry) — use the same syntax in Blume. **Pass through unchanged.** One exception: Fumadocs' looser grammar accepts an id containing whitespace (`[#two words]`); Blume does not parse that as a marker, so rewrite such an id to a hyphenated one and update every link that targets it.\n\n## Code fences\n\n- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.\n- Adjacent fences sharing `tab=\"…\"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.\n\n## OpenAPI\n\n`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list (vendor the spec locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.\n\n## GraphQL\n\n`@fumadocs/graphql` works differently from the OpenAPI flow: **no stub files** — pages are virtual, served through the Loader API. The artifacts to harvest and tear down: `lib/graphql.ts` (`createGraphQL()` — read its `input` for the schema and any per-source routes/labels), the `graphql.staticSource()`/`graphql.loaderPlugin()` wiring in the source config, the `GraphQLPage` component (`createGraphQLPage()`, usually `components/api-page.tsx` — read its playground `endpoint`), and the `@fumadocs/graphql/css/preset.css` import. Map to a `graphql({ spec, endpoint })` entry in the top-level `reference` list (see SKILL.md \"GraphQL\"): SDL files/text and introspection results carry over as `spec` (vendor a URL input locally); a programmatic `GraphQLSchema` instance must be printed to SDL and committed (report); the `createGraphQLPage` playground endpoint becomes `endpoint`. When `createGraphQL()` takes several inputs, or sources carry their own routes, labels, or endpoints, map each one to an entry in the adapter's `sources` (`{ spec, route, label, endpoint }`) instead of the single `spec` shorthand — otherwise a schema is dropped or mounted on the wrong route. Blume groups routes the same way (`<route>/queries/<field>`, `<route>/objects/<type>`), but slugs are re-derived — rewrite inbound links and let `blume validate` catch strays. Then remove `@fumadocs/graphql`, the two lib/component files, and the CSS import. Since there are no generated pages, there is nothing to delete from the content tree — but keep any hand-written conceptual pages under the reference route.\n\n## i18n\n\nA `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's default `dir` parser as-is; locale **file suffixes** (`page.cn.mdx` beside an unsuffixed default-locale `page.mdx`) match the `dot` parser as-is — add `parser: \"dot\"`, no file moves. Under `dot` a folder's `meta.ts` serves every locale, so build it from the default locale's `meta.json` and report any translated folder titles in `meta.<lang>.json`. Report whichever transform you apply.\n\n## Package.json & teardown\n\nRepoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*`/`@fumadocs/*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `lib/source.ts`, `lib/shared.ts`, `lib/layout.shared.tsx` (or `app/layout.config.tsx`), `components/mdx.tsx` / `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).\n\n## Dropped — report these\n\nFolder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full` (approximated as `mode: wide`); header settings from `lib/layout.shared.tsx` with no Blume home; `<DynamicCodeBlock>`/`<InlineTOC>`; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.\n",".agents/skills/blume-migrate/references/gitbook.md":"# GitBook → Blume\n\nGitBook is a hosted editor. This reference covers the Markdown it writes when a space syncs to GitHub or GitLab (Git Sync): `SUMMARY.md` (a space's navigation), `.gitbook.yaml` (where its files live, plus redirects), and, on site-wide Git Sync, `gitbook-docs.yaml` (which space maps to which directory). Theme, logo, header and footer links, custom domain, site redirects, and access control live in the GitBook app, not the repo. Two things set this migration apart:\n\n- **URLs follow `SUMMARY.md`, not the file tree.** Moving a page in the editor changes its URL but not its file. Blume derives route _and_ sidebar from the file path, so the core move is putting each file at its old URL.\n- **Blocks are Liquid-style tags (`{% hint %}`) and raw HTML inside `.md`.** Almost every page becomes `.mdx`, and MDX rejects several things GitBook writes.\n\n## Detect\n\n- **`.gitbook.yaml`** or **`.gitbook.yml`** (GitBook reads both) at the root or in a space directory; **`gitbook-docs.yaml`** for site-wide Git Sync.\n- A **`SUMMARY.md`** (GitBook writes it opening `# Table of contents`; a hand-written one may open `# Summary`), with `* [Title](path.md)` items and `## Group` headings, beside a **`.gitbook/`** folder (`assets/`, `includes/`, maybe `vars.yaml`). `.gitbook.yaml` is optional, so this pair can be the only sign.\n- Pages that open with a `# Title` line and use `{% hint %}`, `{% tabs %}`, `{% content-ref %}`; sync commits titled `GITBOOK-<n>: …`.\n- **Not this format:** `book.toml` + `src/SUMMARY.md` is mdBook. `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit — see [Legacy GitBook CLI](#legacy-gitbook-cli-bookjson).\n\n## First, collect what the repo doesn't hold\n\nDo this while the GitBook site is still published. Nothing moves before step 1.\n\n1. **Old URLs.** `<site>/sitemap.xml` indexes one `sitemap-pages.xml` per published space (`/sitemap-pages.xml`, `/de/sitemap-pages.xml`, …); fetch them all. They list every canonical page, hidden and generated OpenAPI pages included, but not `noIndex`/`noRobotsIndex` pages (take those from `SUMMARY.md`). `<site>/llms.txt` lists the same pages with their `.md` URLs. Save the paths, one per line, outside the content root.\n2. **Site redirects** (Settings → Redirects). The app has no export: `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/redirects?limit=1000` with a token (paginated: while the response has `next.page`, fetch again with `&page=<it>`), or ask the user. With neither, report it as a follow-up.\n3. **Site customization.** `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/customization` with a token. Without one, read the published HTML: logo and favicon URLs go through `~gitbook/image?url=<original>` (decode `url` and download it), the header carries a light logo (`class=\"block dark:hidden …\"`) and, when one is set, a dark one (`hidden dark:block`), and the inline CSS declares the primary color as an RGB triplet, `--primary-original`.\n4. **Spaces not in the repo.** Compare the sitemap index with the repo's directories. A space with `content.directory: null`, or a language or version the site serves but the repo lacks (often GitBook-made translations), has no Markdown here. Ask: export it (Git Sync that space into a branch, or `POST https://api.gitbook.com/v1/spaces/{spaceId}/git/export`) and migrate it; regenerate it with `blume translate` once `i18n` is set; or drop it and redirect its URLs (see URLs and redirects). Report the choice.\n\n## Config\n\nMap what step 3 found or the user supplies; leave the rest at Blume's defaults.\n\n| GitBook | Blume |\n| --- | --- |\n| Site title (`gitbook-docs.yaml` `site.title`) | `title` |\n| Logo (light and dark) | `logo: { image: { light, dark } }`, files in `public/` (SKILL.md covers monochrome SVGs and wordmarks) |\n| Icon (favicon) | `public/icon.svg` or `icon.png` (a dark one → `icon-dark.*`); no config field. An emoji icon has no file: make one or report it |\n| Primary color | `theme.accent` |\n| Default mode, fonts, code theme | `theme.mode`, `theme.fonts.{body,display,mono}`, `markdown.code.theme` (nearest Shiki themes) |\n| Theme style, tint and semantic colors, corner, depth, link, and sidebar styles | drop (report); a root `theme.css` if one matters |\n| Header links; a button-styled one | `navigation.actions`; `navigation.cta` |\n| Announcement | `banner` (`{ content, link, dismissible, id }`) |\n| Footer | `footer.links`, flattened into one row; copyright → `footer.copyright`; group titles and logo drop (report) |\n| Social accounts | `footer.socials` |\n| Social preview image | `seo.image` on the landing page (Blume renders a card for every other page) |\n| Git Sync repository and branch | `github: { owner, repo, branch }`, plus `dir` or `host` when needed; GitLab has no equivalent (report) |\n| Page actions: Markdown and AI providers, MCP, PDF | defaults; `agents.mcp.enabled` plus server output (follow-up); `export` |\n| GitBook Assistant | `ai.assistant` plus server output (follow-up), or drop |\n| Integrations | adapters in `analytics` from `blume/analytics`; others `script()` or report |\n| Custom domain | the DNS record moves to the new host; keep the URL as `deployment.site` unless the host is Vercel or Netlify |\n| Custom subdirectory (`example.com/docs`) | `deployment: { base: \"/docs\" }` |\n| Share links, authenticated access | host-level protection (the deployment docs' **Private docs**); report |\n| `.gitbook/vars.yaml` | `variables` (see Variables) |\n\n**Content root.** `root: ./docs/` is Blume's default `content.root`. With `root: ./`, `git mv` the space (pages, `SUMMARY.md`, `.gitbook/`) into `docs/` — routes are relative to the content root, so they don't change — or scope `content.include` (`references/monorepo.md` §1). Left unscoped, the repo's `README.md` and every stray `.md` publish.\n\n## Navigation: `SUMMARY.md` → folders\n\n### How GitBook builds a URL\n\n- The first `SUMMARY.md` item (`structure.readme`, `README.md` by default) is served at the space root; its own slug (`/readme`) redirects there.\n- Every other page: the space's prefix (see Site structure), then its `## Group`'s slug, each ancestor page's slug, and its own. A page's slug comes from its file name at import (a `README.md` → its folder's name), lowercased; a group's, from its heading (emoji dropped) or the `id` of an `<a id=\"…\">` inside it.\n- **The file's own path doesn't count.** A page filed at `configuration/post-install.md` but listed under group \"Deployment\" → parent `deployment/permissions/README.md` publishes at `/deployment/permissions/post-install`.\n\nCompute every page's URL from `SUMMARY.md`, then check them against the sitemap list; the sitemap wins.\n\n### Put every file at its old URL\n\nOne `git mv` per page keeps both the route and the sidebar:\n\n- A page with no children → `<url>.mdx`; with children → `<url>/index.mdx`; the landing page → `index.mdx` at the content root (`.md` for a page that stays plain). Lowercase every segment: Blume keeps a file name's case, so `OCR.md` would publish at `/OCR` where GitBook served `/ocr`.\n- **`README.md` isn't an index in Blume** (`guides/README.md` publishes at `/guides/README`): every README becomes `index`.\n- Each `## Group` becomes a top-level folder named for its slug.\n- Keep a table of old file → new file → route: links, includes, and image paths are rewritten from it.\n- A file not in `SUMMARY.md` was never published. Delete it, or prefix it with `_` to keep it unpublished, and report it.\n- Delete `SUMMARY.md` once the folders and `meta.ts` files carry its structure; it would publish as a page.\n\n### Order, labels, and display → `meta.ts`\n\n- **Order.** Each folder's `meta.ts` lists its children in `SUMMARY.md` order (`pages`); the content root's lists the loose pages, then the group folders.\n- **Labels.** A page's title is its `SUMMARY.md` link text, and a link title (`[Title](page.md \"Label\")`) → `sidebar.label`. A group folder's `meta.ts` `title` is the heading text, and **a parent page's folder gets `title: <its SUMMARY text>` too** — otherwise the row shows the humanized folder name (`deployment-guides` → \"Deployment Guides\" for a page titled \"Getting started\").\n- **Display.** GitBook shows groups as plain headings and each parent page as one expandable row. Set `navigation: { sidebar: { display: \"group\" } }`, `display: \"flat\"` in each group folder's `meta.ts`, and `sidebar: { hidden: true }` on each parent's `index`: the folder row already links to it, so without this the parent lists twice.\n- **Contents pages.** A parent whose body is empty or only content-refs to its own children → delete those and set `directory: \"card\"` in the folder's `meta.ts`. It inherits: set `directory: \"none\"` on a nested folder that isn't a contents page.\n- **Order Blume can't match.** Loose top-level pages always list above groups and folders. A parent page among loose pages (Overview, Editions ▸, Use Cases) drops below them, and pages after a `***` divider in `SUMMARY.md` (top-level again: `/uninstall`, not `/other/uninstall`) rise above the groups. Move such pages into a parenthesized group folder (`(more)/`, which adds no URL segment) ordered in the root `meta.ts`, or accept the order; report either.\n- **External links** (`* [Website](https://…)`) → `navigation.featured` or `navigation.actions`; report the move.\n- **Hidden pages.** GitBook's `hidden: true` hides a page's whole subtree; Blume's hides one page, so set it on the index and every descendant (a folder whose pages are all hidden leaves the sidebar).\n- A list item that is a YAML block with `type: builtin:openapi` is a generated API reference (see OpenAPI).\n\n### Site structure: `gitbook-docs.yaml`\n\n`site.structure` nests `section-group`, `section`, `space`, and `external-link` entries. Every entry has a `key` (meaningless to Blume), a `title`, and maybe `localizedTitle`; most can carry `draft` or `condition`. Sections and spaces have a `path` and maybe `default: true` (a section group has no `path`); section groups, sections, and external links can have an `icon`; a space can be `hidden` and has `content.directory` (`null` when not synced) and maybe `content.language`. Skip `draft` entries, and treat a `condition` like adaptive content (report it).\n\n- **URL prefixes.** The default section's default space is at the site root. Another space → `/<section>/<space>`; a non-default section's default space → `/<section>`. A site without sections serves its default space at the root and the rest at `/<space>`.\n- **Sections** → a top-level folder each plus a `navigation.tabs` entry (`icon` → Lucide). The default section stays at the content root; add `{ label, path: \"/\" }` to show it as a tab.\n- **Section groups** → a dropdown tab scopes the sidebar by one `path`, so give each section its own tab (the grouping is lost; report) or move them under one prefix (URLs change; pattern redirects).\n- **Spaces within a section:** languages → `i18n` (Blume puts the locale first, site-wide: `/payments/fr/x` → `/fr/payments/x`); versions → `versions` (top-level `/<id>/…`, ids start with a letter); products → a folder each and a `navigation.selectors` dropdown. Redirect the old prefixes.\n- A space's directory can sit inside another's (`./home/fr` in `./home`): separate them, then do the `SUMMARY.md` work per space. A `hidden: true` space is still published but left out of the space switcher: keep its pages, leave it out of `navigation.selectors`, and report it. `localizedTitle` → a per-locale tab label.\n- **External links** (`external-link`, at the root or in a section group) → `navigation.actions` entries (`{ label, href }`; `localizedTitle` → a per-locale `label`, the `icon` drops).\n\n## Content and components\n\n### `.md` or `.mdx`\n\nRename to `.mdx` every page with a `{% … %}` block, `$$` math, a mermaid fence, or a component after conversion. Directives, components, math, mermaid, `package-install`, and `ts2js` are MDX-only: in `.md`, a leftover `{% hint %}` or `:::note` is literal text with a green build (each `{% … %}` tag warns `BLUME_TEMPLATE_TAG` at its line, a `:::note` `BLUME_MD_DIRECTIVE`), and in `.mdx` a leftover `{% … %}` fails it (`BLUME_MDX_SYNTAX`, which `blume validate` and `blume check` report too). Plain pages can stay `.md`; tables, footnotes, fence meta, `<include>`, `{{variables}}`, `[#id]`, and raw HTML work in both. An include is spliced as text, so it's parsed in the including page's format.\n\n### MDX hazards\n\nEach of these fails `blume build` in `.mdx` (`blume check` and `validate` name the ones MDX can't parse as `BLUME_MDX_SYNTAX`, at their line):\n\n- **Unclosed `<img …>` and `<br>`** (`BLUME_MDX_UNCLOSED_ELEMENT` names each). Close them — better, convert images to Markdown.\n- **HTML comments** → `{/* */}`.\n- **A `{` outside code.** GitBook's `\\{{x\\}}` isn't enough; write `` `{{x}}` `` or `\\{\\{x\\}\\}`. Braces inside raw HTML (`<code>CN={{DeviceId}}</code>`, `<td>${EMAIL}</td>`) compile, then fail the render: `blume build` names each as `BLUME_MDX_UNDEFINED_NAME` at its line. Turn `<code>`, `<strong>`, `<a>`, `<p>`, and `<br>` inside table cells into Markdown.\n- **Markdown inside JSX** (`<Tab>`, `<Step>`, `<Frame>`) needs blank lines around it.\n\nGitBook's `\\<` and entities (`&#x20;`, `&#x3C;`) are fine (an escaped `\\<Name` can still warn `BLUME_UNKNOWN_COMPONENT`; the page renders correctly). Strip the U+200B/U+200C characters GitBook leaves at the start of some code lines.\n\n### Blocks\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` (the first line) | frontmatter `title`; delete the line |\n| `{% hint style=\"info\" %}` (`success`, `warning`, `danger`) | `:::info` (`:::success`, `:::warning`, `:::danger`); legacy plugin `tip` → `:::tip`, `working` → `:::note`. A directive takes no `icon`: keep one that matters as `<Callout type=\"<type>\" icon=\"<lucide>\">`, else drop it; inside another directive, give the outer one a longer fence (`::::`) |\n| `{% tabs %}` + `{% tab title=\"X\" icon=\"y\" %}` | `<Tabs>` + `<Tab title=\"X\" icon=\"<lucide>\">`; `fullWidth` drops. Blume switches same-titled tabs together page-wide (`sync={false}` opts a group out) |\n| `{% code title=\"a.js\" lineNumbers=\"true\" overflow=\"wrap\" expandable=\"true\" %}` | fence meta: ` ```js title=\"a.js\" lineNumbers wrap expandable `; `fullWidth` drops. **A fence with no language takes `text`** (` ```text lineNumbers `), or the first option becomes the language (`BLUME_UNKNOWN_CODE_LANGUAGE`) |\n| `<pre class=\"language-x\" data-title=\"…\" data-line-numbers data-overflow=\"wrap\"><code>` | a fence built from those attributes (others drop), entities decoded, `<strong>`/`<em>` stripped |\n| `{% stepper %}` + `{% step %}` | `<Steps>` + `<Step>`. **A step that opens with a heading keeps it as the first line inside `<Step>`**: GitBook publishes it as an anchored heading in the outline, and `title` renders a plain paragraph with no anchor. A bold-line opener → `title` |\n| `{% columns %}` + `{% column %}` | `<Columns cols={n}>` + `<Column>`; widths drop |\n| `<details><summary>T</summary>` (`open`) | `<Expandable title=\"T\">` (`defaultOpen`); a run → `<Accordion>` + `<AccordionItem title=\"T\">`. A title renders inline Markdown, so write a summary's `<code>`, `<strong>`, and `<a>` as Markdown in it: raw HTML there shows as text |\n| `{% content-ref url=\"x.md\" %}`; legacy `{% page-ref page=\"x.md\" %}` | `<Card title=\"<x's title>\" href=\"<x's route>\" />` (the link text is a file name, so look the title up); a run → `<CardGroup>`; links to a parent's own children → `directory: \"card\"` |\n| `[text](x.md#h \"mention\")` | a plain link without `\"mention\"`, its text replaced by the target's title |\n| `{% embed url=\"…\" %}`, often with no `{% endembed %}` | YouTube → `<YouTube url=\"…\" />` (`?t=122` → `start={122}`; `url` ignores `t`); anything else → `<Card title=\"…\" href=\"…\" />`. A caption → `<Frame caption=\"…\">` around it (YouTube's `title` only reaches screen readers) |\n| `{% file src=\"…\" %}caption{% endfile %}` | `[caption](../assets/x.pdf)`, relative to the page: Blume publishes a file a relative link names with the page. For a URL that never changes, put the file in `public/files/` and link `/files/x.pdf` |\n| `<figure><img src alt><figcaption>` | `![alt](relative/path.png)` (optimized); a caption → `<Frame caption=\"…\">` around it; `<div data-with-frame=\"true\">` → `<Frame>`. A raw `<img>` with a relative `src` works too, but unoptimized |\n| `<img class=\"gitbook-drawing\">` | a Markdown image |\n| inline `<img data-size=\"line\">` | a Markdown image (renders at its own size), or `<img src=\"/<public path>\" alt=\"\" height=\"20\" />` to stay text-height (report) |\n| `<picture>` with a dark-mode source | the light image (report) |\n| `<table data-view=\"cards\">` | `<CardGroup cols={3}>` (`data-card-size=\"large\"` → `cols={2}`), a `<Card>` per row: first text cell → `title`, other text → body, the `data-card-target` link → `href`, the `data-card-cover` file → `img`, relative to the page like a Markdown image (`img=\"../assets/cover.png\"`), an `<i class=\"fa-…\">` cell → `icon` |\n| other `<table>` (`data-header-hidden`, `data-search`, cells with `<p>`/`<br>`) | a GFM table (a hidden header's first row becomes the header); a cell's `<ul><li>` can stay inline in `.mdx` |\n| `$$…$$`, a ` ```mermaid ` fence | unchanged, in `.mdx` |\n| Emoji: `:rocket:`, escaped `:ballot\\_box\\_with\\_check:`, `<span data-gb-custom-inline data-tag=\"emoji\">☑️</span>` | the character itself: Blume renders no shortcodes |\n| `<i class=\"fa-x\">:x:</i>` | `<Icon icon=\"<lucide>\" />`, or delete it |\n| `<a class=\"button primary\">`, `<button data-action=\"…\">` | a link or a `<Card>` with `cta`; search and ask buttons drop |\n| `<mark style=\"color:…\">` | `<mark>` or bold; report colors that carried meaning |\n| `## Heading <a href=\"#x\" id=\"x\"></a>` | works as written: the heading takes `x` as its id. `## Heading [#x]` is the tidier spelling |\n| `[^1]` annotations | unchanged: footnotes at the page's end, not hover popups |\n| `{% updates %}` + `{% update date=\"…\" %}` | `type: changelog` pages (SKILL.md \"Changelogs\"), or dated `##` sections |\n| `{% prompt description=\"…\" %}` | `<Prompt description=\"…\">` |\n| `{% if visitor… %}` | keep what the public saw, drop the tags (report) |\n\n### Links\n\n- Every moved file breaks the relative links into and out of it. Rewrite internal links from your table as root-absolute routes (`/deployment/permissions#roles`): Markdown links, content-ref URLs, card `href`s, and `href`s in HTML. A `README.md` target → its folder's route; absolute links to the old domain → routes.\n- Cross-space links `https://app.gitbook.com/s/<spaceId>/<path>` (or `…/o/<orgId>/s/…`) → the target space's prefix plus `<path>`; `GET https://api.gitbook.com/v1/spaces/{spaceId}` gives its `urls.published`. Links into `…/~/changes/…` or to `/broken/pages/<id>` were already broken: fix or report them.\n\n### Images and assets\n\n- Move `.gitbook/assets/` to `assets/` at the space root and replace the string `.gitbook/assets/` with `assets/`, then recompute the paths in every moved page.\n- File names have spaces and parentheses. `![](<../assets/image (1).png>)` and `../assets/image%20(1).png` both work, in a page and in an `_includes/` partial alike: a partial's images are rebased onto the including page in any form.\n- GitBook keeps every upload, so many files in `assets/` may be referenced by nothing. Report them and offer to prune.\n\n### Variables and expressions\n\n- `.gitbook/vars.yaml` → `variables`; `<code class=\"expression\">space.vars.NAME</code>` → `{{NAME}}`.\n- `page.vars.NAME` (from `vars:` frontmatter) has no page scope: write the value inline. Read inside an include, it becomes a prop: `<include name=\"Computer\">/_includes/x.md</include>` with `{{name}}` in the partial (prop names are lowercase).\n- An expression with logic (`a ? \"x\" : \"y\"`) → evaluate it and write the result.\n- Once `variables` is set, an undefined `{{name}}` in prose fails the build: placeholders that are content (`{{DeviceId}}`) go in inline code.\n\n### Includes (reusable content)\n\n- `.gitbook/includes/x.md` → `_includes/x.md` at the content root, and `{% include \"../../.gitbook/includes/x.md\" %}` → `<include>/_includes/x.md</include>` on its own line (a leading `/` resolves from the content root).\n- **Demote every heading in an include one level:** GitBook renders an include's `#` at a page's `##`.\n- Delete includes nothing references. One referenced by ID (`{% include \"/reusable-content/…\" %}`) isn't in the repo: copy it from the app.\n\n### OpenAPI\n\nGitBook has five forms: a `SUMMARY.md` YAML item `type: builtin:openapi` naming an organization spec (`dependencies.spec.ref.spec: <slug>`); `{% openapi src=\"…\" path method %}`; `{% openapi-operation spec=\"<slug>\" path method %}` and `{% openapi-schemas %}`; the older `{% swagger src=\"…\" path method %}`; and hand-written `{% swagger method path baseUrl %}` blocks with `swagger-parameter` and `swagger-response` (oldest: `{% api-method %}`).\n\n- **Specs.** Vendor each one into the repo — a `<slug>`'s file comes from the app's OpenAPI panel or `GET https://api.gitbook.com/v1/orgs/{organizationId}/openapi/{specSlug}/versions/latest/content/raw` — and add `openapi({ spec })` from `blume/reference` with a `navigation.tabs` entry (SKILL.md \"OpenAPI\").\n- **`builtin:openapi`** → set `route` to the old reference's path and keep its parent page's prose under it. Operation URLs change, so redirect each old one in the sitemap (SKILL.md \"OpenAPI\" covers generating them).\n- **Inline operations** → a link or `<Card>` to the generated page. `{% openapi-schemas %}` has no counterpart (report).\n- **Hand-written blocks** → a `<ParamField query|path|header|body=\"…\">` per parameter (a `cookie` has no slot), the responses as titled fences in `<ResponseExample>`, and `api: \"GET /path\"` frontmatter when it's the page's only endpoint (`baseUrl` → `api.server`).\n- `x-codeSamples` carries over; GitBook's other `x-*` extensions are ignored (report).\n\n## Frontmatter\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` / `SUMMARY.md` link text | `title`; where they differ, GitBook published the `SUMMARY.md` text |\n| `SUMMARY.md` link title | `sidebar.label` |\n| `description` (often folded `>-`) | `description` (quote a plain value containing `: `) |\n| `icon` (Font Awesome) | `icon`, in Lucide (see Icons) |\n| `hidden: true` | `hidden: true`, on the page and every descendant |\n| `noIndex: true` | `search.exclude: true` + `noindex: true`, on the page and every descendant |\n| `noRobotsIndex: true` | `noindex: true`, on the page and every descendant |\n| `cover`, `coverY`, `coverHeight`, `tags`, `if` | drop (report); a cover worth keeping → an image at the top of the body |\n| `vars` | inline the values, then drop |\n| keys left by older tooling (`category`, `order`) | drop: GitBook ignored them |\n\n`layout`:\n\n| GitBook `layout` | Blume |\n| --- | --- |\n| every part visible, `width: default` | delete the block |\n| `tableOfContents.visible: false` | `mode: center`; with `title.visible: false` too, `mode: custom` |\n| `outline.visible: false`, or `width: wide` | `mode: wide` |\n| `title.visible: false`, sidebar shown | `mode: frame` |\n| `pagination.visible: false` | `pagination: false` |\n| other parts hidden; a preset (`layout: landing`) | drop (report); for a preset, match the published page |\n\nA Blume mode hides more than one part: `center` and `wide` also hide the outline, and `frame` hides it and the page-end links and footer. Report it where GitBook kept those.\n\nBlume's frontmatter is strict: remove every key the tables drop.\n\n## Icons\n\nGitBook uses Font Awesome names in frontmatter `icon`, `<i class=\"fa-…\">`, tab, hint, and button icons, and section icons. Blume is Lucide-only.\n\n- **Frontmatter:** `node <skill>/scripts/mintlify-codemod.mjs <content-dir>`, then `--write`. Its icon pass is the same Font Awesome → Lucide map, and GitBook frontmatter has none of the keys its other rules touch.\n- **Everything else by hand,** with the table in `references/mintlify.md`. Common GitBook names: `circle-info` → `info`, `clock-rotate-left` → `history`, `sliders` → `sliders-horizontal`, `handshake-angle` → `handshake`, `angles-up` → `chevrons-up`, `puzzle-piece` → `puzzle`, `life-ring` → `life-buoy`, `shield-halved` → `shield`, `file-contract` → `file-text`.\n- **Check every name** against the set Blume bundles; an unknown name renders nothing:\n\n  ```bash\n  node -e 'const b=require.resolve(\"blume/package.json\");const s=require(require.resolve(\"@iconify-json/lucide/icons.json\",{paths:[b]}));for(const n of process.argv.slice(1))console.log(n,!!(s.icons[n]||s.aliases?.[n]))' zap history sliders-horizontal\n  ```\n\n## URLs and redirects\n\nWith every file at its old URL, pages need no redirects. The rest are below. GitBook answers all of them with a 307, except a permanent site redirect (308). Blume's default `status` is 301: keep it for a settled move (`.gitbook.yaml` redirects, recovered automatic redirects), and set `status: 307` where the target may change (group paths, dropped translations).\n\n- **`.gitbook.yaml` `redirects`** (`old/path: new/file.md`, relative to the space) → `{ from: \"/<space prefix>/old/path\", to: \"<new/file.md's route>\" }`.\n- **Site redirects** (`source`, `destination`, `permanent`, `captureWildcard`, `draft`) → `{ from: source, to, status: permanent ? 308 : 307 }`. `destination` is an object, not a URL: `kind: \"external\"` carries `url`; `site-page` (`siteSpaceId`, `pageId`), `site-space`, and `site-section` name ids. Resolve those to the old URL — `GET …/sites/{siteId}/site-spaces` gives each site space's `urls.published` and `space.id`, `…/sites/{siteId}/sections` each section's `urls.published`, and `GET /v1/spaces/{spaceId}/content` each page's `path` within its space — then to the Blume route at that URL. A trailing `*` with `captureWildcard` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`; without it, every match goes to the one `to`. Skip drafts (the list leaves them out by default), and write `from` without the site base.\n- **Automatic redirects.** GitBook quietly 307s every former URL of a moved or renamed page, and lists them nowhere. Recover them: run every historical `SUMMARY.md` (`git log --format=%h -- SUMMARY.md`, then `git show <sha>:SUMMARY.md`) through the URL rule above, add the route each file's path implies, and request each candidate from the live site (`curl -s -o /dev/null -w '%{http_code} %{redirect_url}'`). Each 307 to a page you kept becomes a redirect.\n- **Group paths and the landing slug.** GitBook redirects a group's own path to its first page, and `/readme` to `/`. Add both (`status: 307` for the group paths).\n- **Dropped translations.** GitBook translates group slugs (`/de/zahlungen/…` for group \"Payments\"), so `/de/:path*` → `/:path*` sends most translated URLs to 404s. Diff each language's `sitemap-pages.xml` against the default one, add a pattern per translated group (`{ from: \"/de/zahlungen/:path*\", to: \"/payments/:path*\", status: 307 }`), then each language's catch-all last: patterns are tried in order.\n- **Case.** GitBook matched URLs case-insensitively; Blume doesn't. Lowercased files cover the canonical URLs; report mixed-case inbound links.\n- **Markdown URLs.** GitBook serves `<url>.md`, and so does Blume; redirects move the `.md` URL with its page.\n- Rewrite internal links **before** adding redirects: `blume validate` accepts a redirect's `from` as a link target.\n\n### Heading anchors\n\nGitBook makes heading ids its own way: colons, slashes, and a spaced dash become one hyphen each, dots stay, an id that would start with a digit gets `id-`, and ids stop at 100 characters (`AppConfig:OCSP:UseAuthorizedResponder` → `appconfig-ocsp-useauthorizedresponder`, where Blume writes `appconfigocspuseauthorizedresponder`). Every `#anchor` link GitBook wrote to such a heading fails `blume validate`, and links from other sites land at the top of the page. A rule predicted only 890 of 934 ids on a real site, so read them instead. After `blume build`, from the project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com          # report\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com --write  # pin\n```\n\nIt fetches each old page once (one request at a time, cached in `.pin-heading-ids-cache/`; delete it after), pairs its headings with the build's by text, and appends the old id to each differing source heading — `[#id]` in `.mdx`, `{#id}` in `.md`, `\\{#id\\}` in a partial. Rebuild and rerun until it reports 0. A `NOTE … no source line` is a heading the source doesn't spell out (a component renders it, or it was reworded): pin it by hand. `--only-linked` pins only ids that content links to. A page whose URL changed (a space under a new prefix) is looked up on the old site through `--map`, a JSON list of exact `{ from, to }` entries (patterns are skipped): `dist/blume-redirects.json` works as it is, but a build writes it only when no host adapter is set. Any `BLUME_BROKEN_ANCHOR` left after that was already dead on GitBook: retarget the obvious ones, drop the fragment from the rest, and report them.\n\n### Final check\n\nRequest each old URL from `blume dev` or `blume preview` (both apply pattern redirects) with `curl -sL`, or check it against `dist/` and `dist/blume-redirects.json` (written only when no host adapter is set), then run `blume audit --only redirects` after the build.\n\n## Legacy GitBook CLI (`book.json`)\n\nThe old `gitbook-cli` toolchain and its fork HonKit:\n\n- **`book.json`:** `root`, `structure`, `title`, `description`, `variables`, `plugins`. Map `title` and `description`; `root` → `content.root`; plugins → whatever Blume has built in, else report.\n- **URLs are file paths:** `a/b.md` → `/a/b.html`, a `README.md` → its folder's `index.html`. Keep the files where they are, rename READMEs to `index`, and add an exact redirect from each `.html` URL (a pattern can't strip the extension).\n- **`SUMMARY.md`** nests the same way, but `### Part` headings and `---` lines add no URL segment → `(part)/` group folders. Entries pointing at anchors (`page.md#x`) aren't pages.\n- **Still applies:** `{% hint %}` → directives; `{% include \"./x.md\" %}` → `<include>`; `{{ book.name }}` → `variables` and `{{name}}`; other Nunjucks logic → evaluate and inline; `LANGS.md` → `i18n`, with the default language at the content root; `GLOSSARY.md` → no equivalent (report).\n\n## Teardown\n\n- **Turn off Git Sync** for every mapped space before the converted content reaches the synced branch: GitBook imports each push to it and commits its own edits back. Keep the GitBook site published until the domain moves.\n- Delete `SUMMARY.md`, `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, and the emptied `.gitbook/`.\n- Check CI and scripts that read `.md` files or `.gitbook/assets/`: a Markdown link checker breaks on root-absolute routes and skips `.mdx` (replace it with `blume validate --strict`), and an image de-duplication script that greps `*.md` would now delete images in use. Remove GitBook agent instructions (an `AGENTS.md` block, a vendored GitBook skill).\n- A synced repo usually has no `package.json`: scaffold one (SKILL.md step 6).\n\n## Dropped — report these\n\nSite themes and styles beyond accent, fonts, and mode; footer group titles and logo; page covers; page and update tags; adaptive content and visitor authentication; GitBook Assistant unless the user adds `ai.assistant`; hint icons you didn't keep; button styles and search/ask buttons; `<mark>` colors; `fullWidth` and column widths; non-YouTube embeds as players; dark card covers and theme-aware images; inline image sizing; `{% openapi-schemas %}` and GitBook `x-*` spec extensions; automatic redirects you couldn't recover; case-insensitive URLs; page-group icons; sidebar positions Blume can't match (parent pages among loose pages, `***` dividers, external links); section groups flattened into tabs; spaces and translations not in the repo; integrations with no adapter; site redirects nobody could export; anything set only in the app that the user didn't supply.\n",".agents/skills/blume-migrate/references/github-wiki.md":"# GitHub Wiki → Blume\n\nA GitHub wiki is a git repository beside the main one (`https://github.com/<owner>/<repo>.wiki.git`) that GitHub renders at `https://github.com/<owner>/<repo>/wiki`. It has no config file and no build: every file in a markup format GitHub renders is a page, `_Sidebar.md` and `_Footer.md` add the chrome, and pages link each other with `[[Page Name]]`. Five things set this migration apart:\n\n- **Folders don't count.** A page's URL is its file name, wherever the file sits (`codebase/compiler/Codebase-Compiler-Binder.md` is `/wiki/Codebase-Compiler-Binder`), and relative links and images resolve from `/<owner>/<repo>/wiki/`, never from the file's folder.\n- **`[[Link text|Page Name]]` puts the text first** in Markdown pages. GitHub's docs show the reverse, which is right only in `.mediawiki` pages, and Obsidian's `[[Page|text]]` is the reverse too, so don't point the `obsidian()` source at a wiki.\n- **Blume renders `[[…]]` as literal text.** It warns `BLUME_WIKILINK_UNSUPPORTED` only for a wiki link that names one of the site's pages or has a `|`, so `[[Page that doesn't exist]]` ships with a green build. Every wiki link has to be converted.\n- **The old URLs live on github.com,** which you can't redirect. What you can do there is in URLs and the old wiki below. Never tell the user the old links will redirect, and add no `redirects` for them.\n- **Nothing pins the routes,** so the new site gets clean ones: one flat lowercase route per page, as the wiki had one flat namespace, with the sidebar groups as `(group)/` folders.\n\nScope: Markdown pages fully. Pages in the other formats GitHub renders get converted to Markdown first (Other markup formats). Checked against GitHub's wiki docs, the live rendering of public wikis (microsoft/TypeScript, microsoft/vscode, koreader, ruTorrent, qBittorrent, mpv, gollum, fzf, AdGuardHome) in October 2026, and an end-to-end migration of the microsoft/TypeScript wiki (77 pages). GitHub Enterprise Server wikis use the same files but weren't checked.\n\n## Detect\n\n`blume migrate` can't detect a wiki: name it, `npx blume migrate github-wiki`. A wiki clone has:\n\n- a remote ending in `.wiki.git` (`git remote -v`);\n- `Home.md` (or `Home` in another format) at the root, often `_Sidebar.md` and `_Footer.md`;\n- page files named like URLs (`Getting-Started.md`, `Standalone-Server-(tsserver).md`, sometimes `Roadmap‐2021‐2022.md` with U+2010 hyphens), `[[Page Name]]` links, no config, and usually no `package.json`.\n\n**Not this format:** Docsify keeps a lowercase `_sidebar.md` beside an `index.html` that sets `window.$docsify` (`references/docsify.md`). On macOS and Windows the file system ignores case, so `_Sidebar.md` and `_sidebar.md` are the same name to `ls`, `test -e`, and Node's `existsSync`: check the real case with `git ls-files`.\n\n**Get the pages from the default branch.** `git clone https://github.com/<owner>/<repo>.wiki.git`, then `git remote show origin` names the HEAD branch. GitHub publishes only that branch, so other branches never showed. If the wiki is mirrored from a normal repository (a sync workflow, or a footer saying \"fork it and send a pull request\"), the wiki clone is still what readers saw.\n\n## Where the Blume project goes\n\nThe wiki repository can't host the new site: GitHub runs no Actions in it, serves no Pages from it, and accepts no pull requests. Ask the user where the docs will live:\n\n- **A folder in the main repository** (the usual choice: the docs then change in the same pull requests as the code). Follow `references/monorepo.md` for a code repo's root `package.json`, `dist/`, and workspace.\n- **A new repository**, for docs with their own maintainers.\n\nWithout an answer, convert a copy of the wiki clone in place: `blume.config.ts` and `package.json` at its root, the pages moved into `docs/`, so the user can move the folder as one unit. **Before you start, run `git remote remove origin` in the copy.** Pushing the migrated tree to the wiki would publish `blume.config.ts`, every moved page, and the stubs you prepare later as wiki pages.\n\nWiki history doesn't come along with a plain copy. To keep it for `git log --follow` and blame, bring the wiki in with `git subtree add --prefix=<folder> https://github.com/<owner>/<repo>.wiki.git <branch>` and say so in the report. Every page gets moved and edited either way, so `lastModified: \"git\"` would date every page to the migration commit until it's edited again (Blume follows a rename only when the file didn't change).\n\n## Run the codemod\n\nFrom the copy of the wiki clone (when the docs go into another folder, copy the wiki's files there first and run it from there), before any hand edits:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo>            # report\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write    # write the route table\n# classify the pages in wiki-routes.json (next section)\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo>           # dry run\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo> --write   # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent (a second `convert` skips the pages it already moved). Add `--mirror <owner>/<repo>-wiki` when a repository mirrors the wiki, and `--emoji emojis.json` (from `curl -s https://api.github.com/emojis`) when pages use `:shortcodes:`.\n\n- **`routes`** lists the pages the way GitHub serves them and writes `wiki-routes.json` (page name → new route) and `wiki-files.json` (page name → file) beside `blume.config.ts`. It prints the files GitHub never served, routes that clash, digit-led routes, non-Markdown pages, and its guesses at moved notices and non-content pages. It never overwrites an existing table: it lists what differs instead.\n- **`convert`** does everything in the Navigation, Content, and Images sections below that the table and `_Sidebar.md` decide: moves each kept Markdown page to `docs/(group)/<route>.md` (`.mdx` when it needs MDX) with its frontmatter, converts wiki links, wiki URLs, alerts, fences, headings, named anchors, and duplicate link definitions, moves the wiki's images beside the pages, and writes every `meta.ts`. It prints the `navigation.featured` entries for `blume.config.ts`, the footer's text, and a `REVIEW` line for everything that needs judgment. **Every `REVIEW` line is a to-do**; the sections below say what to do with each.\n- **`check`** (after `blume build`) and **`stubs`** (for the old wiki) are in URLs and the old wiki.\n\nThe sections below are the rules the codemod applies, for checking its output and for anything you do by hand.\n\n## The page list and the route table\n\n**Which files are pages.** Every file whose extension GitHub renders: `.md`, `.markdown`, `.mdown`, `.mkdn`, `.mediawiki`, `.wiki`, `.textile`, `.rst`, `.rest`, `.org`, `.creole`, `.asciidoc`, `.adoc`, `.asc`, `.pod`, `.rdoc`. The page name is the file name without its last extension (`README.html.md.md` is the page `README.html.md`), with spaces read as `-`. Matching is case-insensitive, `-` and space are the same, and U+2010 is not `-`.\n\n- **Not pages:** `_Sidebar.*` and `_Footer.*`, in any folder (GitHub even took `_Sidebar.html.md` as a sidebar). Every other `_`-prefixed file is a page (`_ [obsolete] Config.rest`), and Blume skips `_` files by default, so the rename matters.\n- **Two files with the same name** in different folders share one URL, and GitHub serves one of them (the root one, on the wiki checked). The other was never reachable: leave it out and report it.\n- **Every other file** (images, PDFs, `LICENSE`) is served raw at `/wiki/<path>`, which redirects to `https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`.\n- **GitHub's own list** is `https://github.com/<owner>/<repo>/wiki/_pages` (every page, each linked at its URL). Compare the table with it.\n\n**Old URLs.** `Home` is `https://github.com/<owner>/<repo>/wiki` (`/wiki/Home` 301s there); every other page is `https://github.com/<owner>/<repo>/wiki/<Page-Name>`, percent-encoded.\n\n**New routes.** Each page's route is its name, lowercased, with apostrophes dropped and every other run of characters that aren't letters or digits turned into one `-` (`Standalone-Server-(tsserver)` → `/standalone-server-tsserver`, `What's-new-in-TypeScript` → `/whats-new-in-typescript`, `tsconfig.json` → `/tsconfig-json`); non-Latin letters stay. `Home` becomes `/`. Give one of two clashing pages another route by hand. A route that starts with digits and a separator (`/5-0-release-notes`) would lose them as an ordering prefix, so `convert` pins it with `slug`.\n\n**Classify every page** by editing its entry in `wiki-routes.json`:\n\n- **Content:** keep the route.\n- **A moved notice** (the whole page says it moved: \"This page has moved to <URL>\"): set its entry to that URL, or to the target page's route (with its `#anchor`) when the URL is another page of this wiki. It becomes no page; links to it go straight to the target. A short page that points elsewhere without saying it moved (\"An introduction can be found in the handbook\") is content: keep it.\n- **Not content:** a mirror repository's `README.md`, `SECURITY.md`, or `CONTRIBUTING.md`, test pages, script fixtures, and pages that only say \"deprecated\" with nowhere to point. Set the entry to `null` and list it in the report (SKILL.md step 2).\n\n`routes` prints its guesses for the moved notices and the non-content pages; confirm each one.\n\n## Config\n\nWikis have no config: map what the repository and the special pages say.\n\n| GitHub wiki | Blume |\n| --- | --- |\n| Repository name | `title` (the project's name, or \"`<Project>` wiki\") |\n| Repository description (About) | `description` |\n| The repository | `navigation.repo: \"https://github.com/<owner>/<repo>\"` for the footer link. Set `github: { owner, repo, branch, dir }` only once the pages live in a repository, so **Edit on GitHub** opens the new file. Leave it unset while the home is unknown, and report it |\n| An image at the top of `_Sidebar.md` | `logo`, with the file in `public/` |\n| External links in `_Sidebar.md` | `navigation.featured`: `convert` prints the array |\n| `_Footer.md` links | `footer.links` |\n| `_Footer.md` text | a copyright or license line → `footer.copyright`, as plain text (Markdown stripped); other text → drop (report), or a `PageFooter` layout slot (`defineComponents`). A \"contribute to this wiki\" footer drops with its link: edit links replace it |\n| \"edited this page … · N revisions\" under each title | drop (report). `lastModified: \"git\"` would show the migration date on every converted page |\n| Wiki search, the Pages list, the per-page outline, the clone URL box | built in, or drop |\n| Site URL | none existed. Ask where the site deploys and set `deployment` (SKILL.md). GitHub Pages: https://useblume.dev/guides/markdown-docs-github-pages, with `deployment: { site: \"https://<owner>.github.io\", base: \"/<repo>\" }` for a project site |\n\n## Navigation: `_Sidebar.md` → `(group)/` folders\n\nGitHub showed two things in the right rail: the **Pages** list (every page, alphabetical) and, under it, `_Sidebar.md` rendered as Markdown. The sidebar becomes folders, and every page from the Pages list stays reachable.\n\n- **Each sidebar group becomes a `(group)/` folder** (SKILL.md): it adds the sidebar group and no URL segment, so every route stays the flat one from the table. Its `meta.ts` has `title` (the label as written) and `pages` (every child in sidebar order). The content root's `meta.ts` lists the groups in sidebar order.\n- **Group labels** are whatever heads a list: a bold line (`**News**`), a heading (`## Contributing`), or an unlinked list item with children (`* Testing`, a nested `(group)/` folder with `display: \"group\"`). A heading that links a page (`# [Tips and Tricks](…/wiki/Tips-and-Tricks)`) makes that page the group's own: a real folder with the page as its `index.md`. A heading that links outside (`## [User documentation](https://…)`) is a `navigation.featured` link.\n- **A page with nested pages under it** becomes a real folder named for its route, holding the page as `index.md`, its children, and a `meta.ts` with `pages` and `display: \"group\"`. Each child gets `slug: <its route without the slash>` so its route stays flat. Without `display: \"group\"`, a nested folder lists after every loose page of its parent group, not where the sidebar had it. Blume shows the parent both as the group header and as its first row; that's expected, so don't hide it.\n- **External links** can't sit inside a generated group, so they move to `navigation.featured` (above the sidebar, on every page); report the move. A link to a heading (`[Installation](…/User-patches#installation)`) has no row in Blume: drop it (the page's outline lists it) and report it.\n- **A page listed twice** stays in its first group; report the other entry. `[[Home]]` in the sidebar drops: the home page is `docs/index.md`, listed first, outside every group.\n- **Pages the sidebar doesn't list** were still in the Pages list: they go in a last group, `(more)/` with `title: \"More pages\"`, `display: \"group\"`, `collapsed: true`, and a `pages` list sorted by title (Blume's own order goes by file name, which differs for names like `'this' in TypeScript`). Pages that shared a wiki folder can get their own collapsed group instead (`(codebase)/`); report the groups you made.\n- **No `_Sidebar.md`:** GitHub showed only the Pages list, so the pages sit at the content root (Blume sorts them by title), or in groups by the wiki's folders if it had any; report the choice.\n- **A `_Sidebar.md` inside a folder** replaced the root one for that folder's pages. Usually it's a translation (`ru/_Sidebar.md` beside `ru/Home.ru.md`): map the folder to `i18n` when the pages pair up, else to a `navigation.tabs` entry, and report it.\n- Prose, images, and HTML lists in the sidebar have no place in Blume's sidebar: move a logo to `logo`, put useful prose on the home page, and report the rest.\n\n## Content\n\n### Page files and titles\n\n- **Each kept page** lands at `docs/(group)/<route>.md`, `Home` at `docs/index.md`. It stays `.md` unless it needs MDX (GitHub alerts, mermaid, math), then it's `.mdx` (MDX hazards below).\n- **`title`** is the page name as GitHub showed it above the page: `-` → space, U+2010 → `-` (`Standalone-Server-(tsserver)` → `Standalone Server (tsserver)`, `'this'-in-TypeScript` → `'this' in TypeScript`), quoted in YAML where needed.\n- **Body H1s.** GitHub drew the name as the title and every `#` in the body below it, so a page's H1s are sections, not its title (`# TypeScript 5.3` on a page named \"API Breaking Changes\"). A leading H1 that repeats the page name (ignoring case and punctuation) is deleted. If the page still has an H1, every heading is demoted one level (`#` → `##`, `##` → `###`, …; `######` stays), and a one-line setext heading becomes ATX one level down (`===` → `##`, `---` → `###`). A setext heading over several lines has no ATX form: `convert` reports it, so rewrite it as one `###` line by hand. Heading ids come from text, so demoting changes none.\n- **`sidebar.label`** is the sidebar's text for the page, when it differs from `title` by more than case and `-` versus space.\n\n### Wiki links\n\nEvery `[[…]]` outside code converts. GitHub leaves `[[ … ]]` in fenced code alone (bash tests), and so does `convert`: it skips fenced (in a quote too) and indented code, inline code spans, `<code>` and `<tt>` elements, and HTML comments. An indented block that holds `[[` gets a `REVIEW` line, since list continuation and code look alike there. A code span that runs across lines isn't seen as code, so check any `REVIEW` line that quotes shell syntax (`[[ -f x ]]`).\n\n- **Inside an HTML block** (a line opening with `<div>`, `<p>`, `<table>`, `<details>`, and the like, up to the next blank line), Markdown shows as text, but gollum, the engine behind GitHub's wikis, resolves `[[…]]` before the Markdown is parsed, so it made a link there: `convert` writes `<a href=\"/page-name\">text</a>` there, and a bare wiki URL too. An image tag there gets a `REVIEW` line: write it as `<img src=\"…\" />` with the relative path the line gives (Blume publishes the file with the page), or move it out of the block. A `#` line inside the block is text, not a heading, so it isn't demoted.\n- **`'[[Page]]`** (an apostrophe right before, none right after) is gollum's escape for literal brackets: `convert` leaves it and reports it. Check the live page, and drop the apostrophe if it showed the brackets.\n- **`\\[\\[Page\\]\\]`** converts as a wiki link, because pandoc writes them that way (Other markup formats), with a `REVIEW` line: on a page that was always Markdown, GitHub showed those brackets, so restore them there.\n\n| GitHub (`.md` pages) | Blume |\n| --- | --- |\n| `[[Page Name]]` | `[Page Name](/page-name)`: the text as written, the route from the table |\n| `[[Link text\\|Page Name]]` | `[Link text](/page-name)` |\n| `[[Page Name#anchor]]`, `[[text\\|Page Name#anchor]]` | `[…](/page-name#anchor)`, the anchor as written |\n| `[[text\\|https://…]]` | `[text](https://…)` |\n| `[[Home]]` | `[Home](/)` |\n| a page the wiki doesn't have (GitHub drew it as a link to create the page), or one set to `null` | plain text; reported |\n| a moved notice | a link to its target |\n| `[[image.png]]`, `[[/images/x.png\\|alt=Text]]`, `[[x.png\\|align=center,frame,alt=Text]]`, `[[https://…/x.png\\|alt=Text]]` | an image: `![Text](…)` (Images and files). Any first part with an image extension is an image, whatever follows; options are separated by `,` or `\\|`. A leading `/` is the wiki root. A path the root doesn't have but the page's own folder does resolves there, reported. Options other than `alt=` (`width=`, `align=`, `frame`, stray text) drop; reported |\n| `[[text\\|docs/spec.pdf]]` (a file stored in the wiki) | left as is and reported: copy the file into `public/` and link its root path |\n| `[[_TOC_]]` | deleted: GitHub lists a table of contents among unsupported syntax, and Blume's outline shows the headings |\n\n(In this table `\\|` is the table's escape for a single pipe.) GitHub trims spaces around the pipe (`[[Mailing list | https://…]]`), and inside a Markdown table cell the pipe may be written `\\|`. Pages are looked up the way GitHub did: by name, ignoring case, with `-` and space interchangeable and folders ignored. **Anchors are case-sensitive in Blume** and weren't on GitHub, wherever they appear: when `blume validate` reports one (`#TSDeclFiles` for a heading pinned `[#TsDeclFiles]`), match the id's case.\n\n### Markdown links\n\n| Link | Blume |\n| --- | --- |\n| `https://github.com/<owner>/<repo>/wiki/Page-Name` (any case of owner and repo, `http`, `www.`), `/<owner>/<repo>/wiki/Page-Name`, or bare `Page-Name` / `./Page-Name` | the page's route through the table, keeping `#anchor`, percent-decoded (`%28` is `(`) |\n| `…/wiki`, `…/wiki/`, `…/wiki/Home` | `/` |\n| a mirror repository's copy of a page (`https://github.com/<owner>/<repo>-wiki/blob/<branch>/Page.md`) | the page's route (`--mirror`): the mirror goes stale with the wiki |\n| a bare wiki URL in prose, or `<https://…/wiki/Page>` | `[Page title](/route)` |\n| a page's file by its path (`dir/Page.md`, `…/wiki/dir/Page.md`), which GitHub served raw | the page's route; reported |\n| `#user-content-x` | `#x` (GitHub's ids carry that prefix, so both forms worked there) |\n| relative links that leave the wiki (`../issues/12`, `../blob/main/src/x.ts`, `../../other-repo`) | the absolute URL each resolved to from `https://github.com/<owner>/<repo>/wiki/<page>` (`../issues/12` → `https://github.com/<owner>/<repo>/issues/12`) |\n| a link to a page that doesn't exist (`./formatter.md`, `GLOSSARY.md#x`: GitHub served nothing there) | left as is and reported: it was broken on GitHub too, so retarget it (often the page moved to another repository) or unlink it |\n| `…/wiki/_pages`, `…/wiki/<Page>/_history`, revision URLs | left and reported: they point at the wiki's UI and stop working with it |\n| another repository's wiki | unchanged |\n\n`#123` and `@user` are plain text in wikis (GitHub doesn't autolink them there), so they stay. Bare URLs autolink in both. A raw HTML `<a href>` to a wiki page gets the route too (and a wiki link in an HTML block becomes one); Blume adds `deployment.base` to its root path like a Markdown link's.\n\n### Link definitions\n\n`blume validate` checks every link reference definition the page uses, including ones nothing cites; when a label is defined twice it checks only the first, which CommonMark uses. Wikis kept by scripts collect definitions that never rendered:\n\n- **A later definition of a label** never rendered: `convert` deletes it.\n- **A Foam or VS Code block** of autogenerated definitions (`[//begin]: # \"Autogenerated link references…\"` … `[//end]: #`) goes too: once the wiki links are converted, nothing uses it.\n- **A line like `[1]: <src/file.ts - function x(`** with no closing `>` isn't a definition at all: it shows as text, on GitHub and in Blume alike. Leave it, or delete it if it's leftover tooling syntax, and report it.\n\n### GitHub Markdown\n\n| GitHub | Blume |\n| --- | --- |\n| `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` at the start of a line | `:::note`, `:::tip`, `:::note`, `:::warning`, `:::danger`, in `.mdx`. Blume renders an alert itself as the same callout in `.mdx`, so `convert` leaves one indented in a list, or with lazy lines, as written and makes the page `.mdx`. One with text after its marker, or no body, is reported: convert it by hand. In a `.md` page an alert renders as a quote with a literal `[!NOTE]` and warns `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. The older `> **Note**` form is a plain quote on GitHub today: leave it |\n| ` ```mermaid ` | the same fence, in `.mdx` |\n| ` ```math ` | a `$$` block, in `.mdx` |\n| `$…$`, ``$`…`$``, `$$…$$` | by hand: `$$…$$` in `.mdx` (inline ones stay in their sentence; SKILL.md \"Math\") |\n| ` ```geojson `, `topojson`, `stl` | a code block: maps and 3D models drop; reported |\n| Fence languages | lowercased (`TypeScript` → `typescript`), which only tidies: Blume reads them in any case, as GitHub did. `posh` → `powershell`. Any other name Shiki lacks renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: change it to a Shiki language, or `text` |\n| `:emoji:` shortcodes | the character, with `--emoji`. GitHub slugged a heading's shortcode name, so a heading with one is reported: convert it and pin the old id (`## ⚠️ Known issues [#warning-known-issues]`) |\n| `<a name=\"x\"></a>` in a heading | deleted, with `[#x]` pinned on the heading (Blume would give the heading the id `x` anyway). Elsewhere it stays: links to `#x` work, and `blume validate` resolves them |\n| task lists, tables, footnotes, strikethrough, autolinks, `<details>` | unchanged |\n| `style=`, `<script>`, `<iframe>`, `<form>`, `<input>` | by hand: GitHub's sanitizer removed them, so readers never saw them, while Blume renders raw HTML as written. Delete them, or keep one deliberately and report it |\n\nHeading ids need no pinning beyond the rows above: GitHub and Blume both slug with `github-slugger` (identical ids for all 710 headings on the wiki pages checked). The exception is a heading that ends in an emoji or symbol after a space: GitHub keeps the trailing dash (`## Features ✨` → `#features-`), Blume drops it (`#features`); `blume validate` reports the links to fix. Deleting a leading H1 can shift the `-1` suffix of a later heading with the same text; `blume validate` reports the links that break. `pin-heading-ids.mjs` can't read a wiki anyway: GitHub puts each id on a link beside the heading, not on the heading.\n\n### MDX hazards\n\nMDX rejects things GitHub's Markdown accepts, and a page renamed to `.mdx` fails `blume build` on them. In prose (outside code), `convert` fixes the mechanical ones in the pages it renames: a `<` that doesn't start a tag (`TypeScript >=1.6, <7` → `&lt;7`), bare `{` and `}` (escaped `\\{`), HTML comments (`{/* */}` on one line), unclosed void tags (`<br>` → `<br />`), `<https://…>` autolinks, and indented code (MDX has none, so a top-level indented block gets a fence; a backslash-escaped backtick doesn't start inline code, so braces after it get escaped too). It reports the rest:\n\n- **A tag-like word** (`Array<string>` outside code, `<placeholder>`): MDX reads it as an unclosed JSX element. Put it in backticks.\n- **Raw HTML blocks** (`<details>`, `<div>`, `<table>`) with Markdown inside: MDX parses that Markdown only with blank lines around it, and fails on tags interleaved with list items. Add the blank lines, or turn the block into Markdown.\n- **Indented code inside a list:** fence it by hand, at the list item's indentation.\n\n### Other markup formats\n\nConvert each non-Markdown page to Markdown before `convert` (which skips and lists them): `pandoc -f <format> -t gfm-raw_html --wrap=preserve page.<ext> -o page.md` (`mediawiki`, `textile`, `rst`, `org`, `creole`, `asciidoc`, `pod`). `.rdoc` has no pandoc reader: convert it by hand. Delete the original, point the page's entry in `wiki-files.json` at the new `.md` file, and rerun `convert`.\n\n- **MediaWiki pages put the target first,** `[[Page Name|text]]`, as MediaWiki does. Pandoc turns them into `[text](Page_Name \"text\")`, with `_` for spaces, and `convert` maps those to routes.\n- **In the other formats** pandoc escapes `[[…]]` to `\\[\\[…\\]\\]`, and `convert` reads those as Markdown wiki links (text first).\n- Heading ids differ in these formats. Compare each converted page with the live one, and list every conversion in the report.\n\n## Frontmatter\n\nGitHub reads no frontmatter, so wiki pages have none. Each page gets `title`, plus `sidebar.label` and `slug` where the sections above say so. If a page opens with a `---` block anyway, check the live page for what readers saw, keep that as content or drop it, and report it.\n\n## Images and files\n\n- **Files stored in the wiki** (`https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`, `https://github.com/<owner>/<repo>/wiki/<path>`, or a relative `<path>`, which resolves from the wiki root whatever the page's folder) go away with the wiki. `convert` moves each referenced one into `docs/` at the same path (`docs/images/x.png`) and points the page at it relatively (`../images/x.png` from a `(group)/` page): Blume optimizes and validates relative Markdown images.\n- **A relative image that pointed at nothing** (it climbs out of the wiki root, like `../../screenshots/x.png`, or names a file the wiki doesn't have) was broken on GitHub too, and fails `blume build`: `convert` drops it and reports it.\n- **Uploaded attachments** (`user-images.githubusercontent.com`, `github.com/user-attachments/assets/…`, `private-user-images.githubusercontent.com`) are served outside the wiki and keep working for a public repository. `convert` reports each one: download and vendor it next to the pages (one request at a time) unless the user objects. From a private or internal repository they need a GitHub login, so vendoring is required.\n- **`https://github.com/<owner>/<repo>/blob/<ref>/<path>`** shown as an image is an HTML page anywhere but GitHub: `convert` uses `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`.\n- **Raw `<img>` alone on its line** becomes a Markdown image; its `width`/`height`/`align` drop and are reported. A raw `<img>` of a wiki file inside other HTML (a table cell, a `<p>`) is reported: its `src` still reads from the wiki root, which isn't the page's new folder. Copy the file into `docs/` at the same path, as `convert` does for the others, and point `src` at it relative to the page (`../images/x.png`), which Blume publishes with the page, unoptimized; or make it a Markdown image.\n- **Linked files** (PDFs, archives, an image opened as a link) are reported: copy each into `docs/` at the same path and link it relative to the page, which publishes it with the page, or into `public/` and link its root path for a URL that never changes.\n- `#gh-dark-mode-only` / `#gh-light-mode-only` pairs: keep the light image (report).\n- Files nothing references stay in the wiki (other sites may hotlink them): report them, don't copy them.\n\n## URLs and the old wiki\n\nEvery old URL is on github.com. GitHub has no redirect setting for wiki pages, and it sanitizes their HTML (no `<script>`, no `<meta>` refresh), so nothing on the old wiki can forward a reader. Don't add Blume `redirects` for them either: they would only match paths on the new site. The route table is the record instead: each old page name maps to its new route, a target URL, or `null`.\n\n**Check the table against the build** after `blume build`: `node <skill>/scripts/github-wiki-codemod.mjs check` confirms every route in the table exists in `dist/` (`dist/client/` on a server build), anchors included. Given the old URLs instead (every line of a saved `_pages` list, for one), it prints each one's new route: `ok`, `moved` for a moved notice's outside target, `DROPPED` for a page set to `null`, or `MISSING` for a page the table lacks.\n\n**What the user can do on the old wiki** (prepare it, but never commit or push it, even with push access: `stubs` only writes files, and whether and when anything goes to `<repo>.wiki.git` is the user's decision, after the new site is live):\n\n1. **Restrict editing** (Settings → Features, under Wikis: **Restrict editing to collaborators only**) as soon as the migration starts, so the content stops drifting.\n2. **Stop anything that syncs the wiki, before pushing anything to it.** Otherwise it reverts the stubs or copies them on. Look for a workflow triggered `on: gollum` (wiki edits) in the main repository, a mirror repository (`<repo>-wiki`) whose workflow pushes to `<repo>.wiki.git` on push or on a schedule, a copy of that workflow inside the wiki itself, and wiki-publishing actions. List every one you find.\n3. **Point readers at the new site:** a notice at the top of `Home` and `_Sidebar.md` (the sidebar shows on every page). In a fresh clone of the wiki (not the Blume project: `stubs` refuses to run beside `blume.config.ts`), with `wiki-routes.json` and `wiki-files.json` copied in (don't commit them there): `node <skill>/scripts/github-wiki-codemod.mjs stubs --site https://docs.example.com --notice-only --write`.\n4. **Or replace every page with a link stub,** so deep links from issues, search results, and bookmarks land one click from the new page: the same command without `--notice-only`. Each page keeps its file name, so its URL keeps working; `_Footer` files go, `_Sidebar.md` becomes the notice, images stay for sites that hotlink them, and pages set to `null` are listed for the user to decide. `--site` is the new site's URL with its base path, so regenerate the stubs once the real URL is known.\n5. **Disable the wiki** (Settings → Features → Wikis), once the repository's own links point at the new site. GitHub hides every page, stubs included, without erasing them. It's the user's call; it's never required.\n\nWhether search engines matter: `curl -sI https://github.com/<owner>/<repo>/wiki | grep -i x-robots-tag` prints `none` for a wiki GitHub keeps out of search results (GitHub indexes only wikis with 500+ stars and editing restricted to collaborators). Without it, search results keep sending readers to the old pages for a while, and the stubs are what they find.\n\n**Inbound links in the main repository.** Search its clone with `git grep -n -i -E 'github\\.com/<owner>/<repo>/wiki'`: README, CONTRIBUTING, `docs/`, `.github/ISSUE_TEMPLATE/`, `SECURITY.md`, code comments and test fixtures, `package.json` `homepage`. A large repository doesn't need a full clone: `git clone --depth 1 --filter=blob:none --sparse <url>`, then `git sparse-checkout set --no-cone '/*' '!/tests/baselines/'` (excluding its biggest generated folders). Map each URL through the table with `check`, which also verifies its anchor against the build and suggests ids for one that's gone:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs check https://github.com/<owner>/<repo>/wiki/FAQ#common-bugs\n# MISSING ANCHOR  …/wiki/FAQ#common-bugs → /faq#common-bugs (ids like: common-bugs-that-arent-bugs)\n```\n\nAn old link can carry an anchor that was already dead on GitHub: fix it to a real id, don't copy it. Rewrite each link with the new site's origin (or ask first, when the user only wants a report), and set the repository's About → Website to the new site if it pointed at the wiki. Links in issues, pull requests, and other repositories can't be rewritten: the stubs catch them. List the files you changed or would change.\n\n### Leftover check\n\nRerun until it prints only code samples you mean to keep:\n\n```bash\ngrep -rnE '\\[\\[|user-content-|(^|\\s):[a-z0-9_+][a-z0-9_+-]*:(\\s|$)' docs\ngrep -rniE 'raw\\.githubusercontent\\.com/wiki/|github\\.com/<owner>/<repo>/wiki' docs   # any case of owner and repo\n```\n\nAn alert left in a `.md` page warns `BLUME_MD_GITHUB_ALERT`, and an unknown fence language `BLUME_UNKNOWN_CODE_LANGUAGE`, so `blume build` lists those.\n\n## Teardown\n\n- **Files left behind.** Converting in place leaves the pages you didn't keep where they were: moved notices, `null` pages, the files GitHub never served, and unreferenced images. `convert` lists them. Delete them from the project (nothing under the root publishes, but they confuse later edits): the wiki keeps them, and `wiki-files.json` records their paths for the stubs. Delete `_Sidebar.md` and `_Footer.md` once `convert` has harvested them (a later `convert` then leaves the `meta.ts` files as they are).\n- **Wiki tooling** (a `package.json` for a Markdown generator, link-rewriting scripts, sync scripts and workflows) stays only if it still applies to the pages: run each script in its dry or check mode against `docs/` and see whether any page still uses the syntax it converts. If none does, it goes, with the dependencies only it used. Say which you kept.\n- **Lockfiles:** a mirror repository's `.gitignore` often ignores `package-lock.json` or `yarn.lock`. Remove those lines, so the lockfile SKILL.md step 6 regenerates gets committed.\n- Keep `wiki-routes.json` and `wiki-files.json` beside `blume.config.ts` until the stubs are pushed and the inbound links are rewritten.\n- Scaffold `package.json` (SKILL.md step 6) if the docs' new home has none.\n\n## Dropped — report these\n\nThe Pages list and the clone box; footer text other than a copyright line, and its link; sidebar prose, images, anchor links, and duplicate entries; external sidebar links moved to `navigation.featured`; the per-page \"edited … · N revisions\" line; image sizes and wiki image options; images that were broken on GitHub; maps and 3D models; theme-aware image pairs; raw HTML GitHub had stripped; links to missing pages (GitHub's create-page links) and links that were already broken; shadowed link definitions; pages left out (mirror files, tests, fixtures, deprecated stubs) and moved notices mapped to their targets; pages GitHub never served (same-name shadows); non-Markdown pages converted with pandoc; wiki-UI links (history, revisions, `_pages`); and every old URL, which stays on github.com: say what you prepared for it (notice, stubs, or neither), which sync workflows must stop first, and that nothing redirects.\n",".agents/skills/blume-migrate/references/jekyll.md":"# Jekyll (Just the Docs) → Blume\n\nJekyll is a Ruby static site generator: a `_config.yml`, Markdown pages with front matter, Liquid templating (`{% … %}`, `{{ … }}`) in every page, and Kramdown as the Markdown engine. Docs sites get their navigation from a theme, and the common docs theme is **Just the Docs** (JTD). The work is turning JTD's front-matter navigation into folders and `meta.ts` without moving a URL, and converting the Kramdown and Liquid in every page. Watch for six traps:\n\n- **Navigation lives in front matter** (`parent`, `nav_order`), not in folders. URLs come from the file path or `permalink`, so the nav tree and the file tree often disagree.\n- **Kramdown attribute lists (`{: .note }`) and Liquid (`{% include %}`, `{{ site.x }}`) render as literal text in a Blume `.md` page, with a green build.** Each warns at its line (`BLUME_TEMPLATE_TAG` for Liquid, `BLUME_MD_ATTRIBUTE_LIST` for an IAL). In `.mdx` both fail the build, and `blume check` names each line first: `BLUME_MDX_SYNTAX` for Liquid, `BLUME_MDX_ATTRIBUTE_LIST` for an IAL. Callouts are IALs, so most pages need converting.\n- **URLs end in a slash** (`permalink: pretty`, `/a/b/`) or in `.html` (the default), and relative links were written against those URLs.\n- **The page heading is the body `# H1`.** Front matter `title` is the nav label and `<title>`.\n- **Callout names are the site's own**, declared in `_config.yml`. An IAL whose name isn't declared, or is misspelled, rendered a plain paragraph.\n- **What a page shows depends on Liquid and CSS that ran around it** (`jekyll.environment`, `{% if %}`, `_data`, rules in `_sass/custom` that hide sections or add text). The built site is the record of what readers saw.\n\nScope: **Jekyll 4.x with Just the Docs** (gem or `remote_theme`) is supported, and so is a JTD site that GitHub Pages builds with its own Jekyll 3 (differences flagged below). Checked against Jekyll 4.4.1, Just the Docs 0.12.0 (navigation as of 0.10: unlimited depth, `ancestor`), kramdown 2.5.2 with kramdown-parser-gfm 1.1.0, jekyll-redirect-from 0.17.0, GitHub Pages' gem set (`github-pages` 232, Jekyll 3.10.0), and an end-to-end migration of artineering-io/flair-docs (61 pages). **Plain Jekyll and other doc themes are manual** (see the end): the codemod's Kramdown, Liquid, link, and redirect passes apply to any Jekyll site; only the navigation is JTD's.\n\n## Detect\n\n- **`_config.yml`** at the root or in `docs/` (GitHub Pages' \"deploy from `/docs`\"). It's generic Jekyll.\n- **Just the Docs:** `theme: just-the-docs` or `remote_theme: just-the-docs/just-the-docs` (also the old `pmarsceill/just-the-docs`, with or without `@ref`) in `_config.yml`, or `gem \"just-the-docs\"` in the `Gemfile`. Pages carry `nav_order`/`parent`.\n- **Not Jekyll:** a `_toc.yml` beside `_config.yml` is Jupyter Book; `hexo` in `package.json` is Hexo. A `_config.yml` holding only `theme: jekyll-theme-*` publishes a repo's README with GitHub Pages: one page.\n- **Which Jekyll:** a `Gemfile` with `gem \"jekyll\", \"~> 4…\"` is 4.x. `gem \"github-pages\"`, or no `Gemfile` with a `remote_theme`, is GitHub Pages' Jekyll 3 (3.10 today), which also forces nine plugins on (`jekyll-optional-front-matter`, `-readme-index`, `-titles-from-headings`, `-relative-links`, `-default-layout`, `-github-metadata`, `-gist`, `-paginate`, `-coffeescript`): front-matterless Markdown files are pages, `README.md` is its folder's index when there's no `index.md`, an untitled page takes its first heading as `title`, and `[x](page.md)` links work.\n\n**Where the Blume project goes:** in the Jekyll source directory (the folder holding `_config.yml`), beside `_includes/` and the pages. Set `content.root: \".\"` and scope `content.include` to the page files (`[\"*.{md,mdx}\", \"docs/**/*.{md,mdx}\"]`), and list Jekyll's own `exclude` Markdown (`README.md`, `CHANGELOG.md`) plus `vendor/**` and `node_modules/**` in `content.exclude`, which adds to the defaults `\"**/_*\"` and `\"**/.*\"` (`references/monorepo.md` §1). `_includes/` and `_data/` stay unpublished because of their underscore. In a code repo whose Jekyll source is `docs/`, that puts `package.json` in `docs/` and keeps `dist/` out of the repo root.\n\n## Build the old site first\n\nThe old build is your URL list, your nav tree, your heading ids, and the record of what rendered. Build a copy in a folder outside the repo (`<old>` below, an absolute path), with a project-local bundle. CI usually sets `JEKYLL_ENV=production`, which switches analytics and any `jekyll.environment` branches in includes. From the Jekyll source directory:\n\n```bash\nrsync -a --exclude .git --exclude _site ./ <old>/src/ && cd <old>/src\nbundle config set --local path vendor/bundle && bundle install\nJEKYLL_ENV=production bundle exec jekyll build -d <old>/site\ncd <old>/site && find . -name '*.html' -not -path './assets/*' | sed 's|^\\.||; s|/index\\.html$|/|' | sort > <old>/old-urls.txt\n```\n\nNo `Gemfile` (a GitHub Pages `remote_theme` site): write one with `gem \"github-pages\", group: :jekyll_plugins`. Recent Rubies moved `csv`, `base64`, `bigdecimal`, and `logger` out of the default gems, so under Bundler an older Jekyll fails to load them: add the one it names to the copy's `Gemfile`. If no build works, use the live site (`pin-heading-ids.mjs --old <url>`) and its `sitemap.xml`, rebuild the nav by hand, and say so.\n\nIn `old-urls.txt`, a slashed entry is a page. An `.html` entry is a page on a non-`pretty` site, `404.html`, or a jekyll-redirect-from stub, which GitHub Pages serves without the `.html` (`/old` from `old.html`): `redirects.json` in the build lists those. Non-page files Jekyll copied (`LICENSE`, `.bat` scripts, a `Rakefile`) were published by accident unless a page links them: leave them out and report them. On Jekyll 4, a `.md` file with no front matter wasn't a page either: Jekyll copied it raw.\n\n## Run the codemod\n\nFrom the Jekyll source directory, dry-run the bundled codemod against the old build, then apply it once, before any hand edits:\n\n```bash\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site .          # report only\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site --write .\n```\n\nIt's zero-dependency and deterministic, and it runs once: `--write` always writes `jekyll-migration.json` and refuses a folder that already has one, because a second pass would take each page's next H1 as its title and convert what `{% raw %}` protected. To run it again, start from a fresh copy of the Jekyll source. It reads `_config.yml` (`callouts`, `url`, `baseurl`, `permalink`, `defaults` permalinks, `exclude`, plain values for `{{ site.x }}`) and every page and Markdown partial in `_includes/`, and converts what maps exactly:\n\n- callout IALs → directives (Callouts below), `.label` → `<Badge>`, `.no_toc` and `{#id}` on headings → `[!toc]` and `[#id]`, and JTD's utility and button classes dropped; a malformed IAL, or one naming a class `callouts:` doesn't declare, rendered a plain block, so it goes and is reported;\n- the in-page TOC (the list, its `<details>`, its heading, a partial holding only a TOC), `{::comment}`, ALDs;\n- the body H1 → `title` (a different old `title` → `sidebar.label` and `seo.title`), with a variable it showed written in as its value, since Blume leaves front matter as written; an H1 inside an HTML comment or `<pre>` is skipped. The front matter keys follow the table below;\n- `{% include x.md k=\"v\" %}` and `{% include_relative %}` → `<include k=\"v\">/_includes/x.md</include>` (nested ones too, `{{ include.k }}` → `{{k}}`); a parameter the partial reads but the tag leaves out printed nothing in Jekyll, so it's passed as `k=\"\"`. Also `{{ site.x }}` → `{{x}}` variables, `{{ site.baseurl }}`, `relative_url`, `absolute_url`, `{% link %}`, `{% highlight %}`, `{% comment %}`, and `{% raw %}` (unwrapped, its content untouched);\n- links: relative links resolved against each page's old URL, `.md`, `.html`, own-origin, and trailing-slash links → routes; relative Markdown images rebased from the page's new folder, and a relative raw HTML `src` → a root path (Jekyll resolved it against the page's old URL, which the new folders don't mirror);\n- with `--old`, the sidebar the old build rendered → folders, `slug`s, and `meta.ts` files (Navigation below);\n- each page that now needs MDX → `.mdx`, with void tags self-closed, comments as `{/* */}`, a bare `<` as `&lt;`, autolinks as links, and page `<script>`/`<style>` removed, inline code left as written; partials such a page includes get the same;\n- `redirect_from`, `redirect_to`, `.html` URLs, and the variables → `jekyll-migration.json`, for the config:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport migration from \"./jekyll-migration.json\" with { type: \"json\" };\n\nexport default defineConfig({\n  redirects: migration.redirects,\n  variables: migration.variables,\n});\n```\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each: HTML includes, Liquid logic, unknown front matter, MDX hazards (a brace in prose, an indented code block), the CSS rules that hid or added content, and the theme files. A `{{ name }}` that `{% raw %}` kept as written collides with a variable of the same name, which Blume fills in everywhere, code included: rename the variable (its key in `jekyll-migration.json` and the `{{name}}` references the codemod wrote). A site whose `_config.yml` has no `callouts:` (before JTD 0.4 added the option, sites styled `.note`/`.warning` in `_sass/custom/custom.scss`) keeps its callout IALs, and the dry run says so: read those rules for the names, then pass `--callouts note,warning` on the write run.\n\nAfter the first `blume build`, pin the old heading ids, rebuilding until it reports 0. Jekyll's GFM ids drop underscores (`f_position` → `#fposition`) where Blume keeps them, so about one id in twenty differs:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old>/site --write\n```\n\n## Config: `_config.yml` → `blume.config.ts`\n\nApply `defaults:` (front matter defaults) to the pages in each `scope.path` that the codemod didn't see through: they often set `layout`, and sometimes `parent` or `nav_exclude`.\n\n| Jekyll | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `url` | `deployment.site`, unless the target is Vercel or Netlify (SKILL.md). GitHub Pages with a custom domain needs it |\n| `baseurl` (`/repo`) | `deployment: { base: \"/repo\" }`. **The deployed URL decides:** the GitHub Pages starter workflow passes `--baseurl` from the Pages settings, so a custom domain (a `CNAME`) serves at the root whatever `_config.yml` says |\n| `exclude` / `include` | `content.exclude` / `content.include` (Detect) |\n| `permalink`, `defaults`, `collections` | URLs; above; Collections and posts |\n| `plugins` | Plugins |\n| `kramdown.syntax_highlighter_opts.block.line_numbers: true` | no site-wide switch: drop it and report it. Code without line numbers copies cleanly |\n| `kramdown.toc_levels`, and pages built on H4 sections | Blume's outline shows H2–H3, where Kramdown's TOC listed every level. Count the H4s: when they carry the page (attribute references), set `toc: { maxHeadingLevel: 4 }`; else report it |\n| other `markdown`, `kramdown`, `highlighter`, `sass`, `compress_html`, `liquid` keys | drop |\n| `github_username`, `twitter`, `social.links`, a Discord link | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube) |\n| `repository` (jekyll-github-metadata) | `github: { owner, repo }` |\n| `logo` | `logo`, file moved to `public/`. JTD shows the logo _instead of_ the title, so a wordmark needs `logo: { image, text: \"\" }`; a custom `_includes/title.html` that shows both keeps the text |\n| `favicon_ico` | the file at `public/favicon.ico` (no config field) |\n| `color_scheme: dark` | `theme.mode: \"dark\"`. `light`/unset → omit. A custom scheme (`_sass/color_schemes/<name>.scss`): its `$link-color` → `theme.accent` (`$blue-000` is `#2c84fa`; look the rest up in the theme's `_sass/support/_variables.scss`), and an `@import \"./color_schemes/dark\"` in it → `theme.mode: \"dark\"` |\n| `aux_links` (`\"Label\": \"url\"` or `[\"url\"]`) | `navigation.actions: [{ label, href }]`. `aux_links_new_tab` → drop (Blume opens `http(s)` header links in a new tab) |\n| `nav_external_links` (`title`, `url`) | `navigation.featured` (above the sidebar, where JTD listed them below the pages) |\n| `search_enabled: false` | `search: false`. `search.*` options → drop |\n| `nav_enabled: false` | no site-wide switch: `mode: center` on the pages that showed no sidebar |\n| `mermaid` | delete; ` ```mermaid ` renders natively in `.mdx` |\n| `ga_tracking` | `googleAnalytics({ id })` from `blume/analytics`, one per `G-` id in the comma list. A `UA-` id no longer collects data: report it |\n| `gh_edit_link` + `gh_edit_repository` + `gh_edit_branch` | `github: { owner, repo, branch }`. `gh_edit_source: docs` → `github.dir: \"docs\"` when `blume.config.ts` sits there |\n| `last_edit_timestamp` | `lastModified: \"frontmatter\"` when pages set `last_modified_date` (the codemod maps it), else drop: JTD showed no date without it |\n| `footer_content` / `_includes/footer_custom.html` | links → `footer.links`, social links → `footer.socials`; copyright text → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), and turn a Liquid year (`{{ \"now\" \\| date: \"%Y\" }}`) into `${new Date().getFullYear()}` in a template literal; other text → drop (report) or a `Footer` layout slot (`defineComponents`) |\n| `callouts`, `heading_anchors`, `enable_copy_code_button`, `back_to_top*`, `nav_sort`, `nav_error_report`, `ga_tracking_anonymize_ip`, `gh_edit_link_text`, `gh_edit_view_mode` | drop (the codemod read `callouts`) |\n\n## Navigation: front matter → folders and `meta.ts`\n\nJTD builds one tree from `title`, `parent`, `grand_parent`/`ancestor`, and `nav_order`, wherever the files sit, and shows it as a collapsible tree with only the current section open: set **`navigation.sidebar.display: \"group\"`**. The codemod rebuilds the tree from the sidebar the old build rendered, so `nav_order` ties, `child_nav_order: reversed`, and `nav_sort` are already resolved, and keeps every URL:\n\n- A parent at `dir/index.md` with its children in `dir/` (the layout JTD's docs recommend) moves nothing.\n- A parent that's a leaf file with its children beside it (`docs/setup.md`, `docs/install.md`) becomes a `(group)` folder: `docs/(setup)/index.md` with `slug: docs/setup`, and the children inside it keep their URLs.\n- A child whose file sits elsewhere moves under its parent with `slug: <old route>`. A child _section_ whose folder sits elsewhere is reported: `git mv` it into the parent's folder (into a `(group)` folder, its URLs stay; into a real one, pin each page's `slug`), or leave it. The parent's `meta.ts` already lists it by its folder name.\n- **A `parent` title that names two pages** (two sections both titled \"Setup\", told apart by neither `grand_parent` nor `ancestor`): JTD listed the child under both. Blume lists a page once, so it stays under the first and the codemod reports it.\n- Each folder gets a `meta.ts`: `pages` in the old order; `title` only where Blume's label for the folder (its name, humanized) differs from JTD's; `directory: \"accordion\"` where the old parent page listed its children (JTD's `has_toc`, read from the old build), and `\"none\"` below it where a page didn't, since `directory` inherits.\n- **The duplicate index row:** Blume links a folder's `index` from the group row and lists it again as the first row, where JTD showed it once. The codemod hides that row (`sidebar.hidden: true`); a page the group row links stays in the sitemap, search, and `llms.txt`. `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`, flags a hidden index whose `title` differs from its folder's `meta.ts` `title`, unless its `sidebar.label` names the folder title. An index whose H1 differed from its JTD title gets that title as `sidebar.label`, which is the section label, so it passes too. Where neither the page's `title` nor its `sidebar.label` matches the section label (JTD always labeled a section with its page's `title`, so this is rare), the codemod leaves the row visible and says so. The check accepts three ways out: match the two titles, set the page's `sidebar.label` to the folder title, or show the row (remove `sidebar.hidden`).\n- Pages outside the old sidebar (`nav_exclude`, no `title`, a `parent` that matched nothing) get `sidebar.hidden: true`, which also takes them out of search, the sitemap, and `llms.txt`, where JTD still searched them: set `search: { indexing: { includeHiddenPages: true } }` if they should stay searchable. A hidden home page is the exception: the root URL serves it, so it stays listed. An external `nav_external_links` entry is listed for `navigation.featured`.\n\nWhat's left for you:\n\n- **A wrapper folder JTD never showed** (the template's `docs/`, any folder whose pages sit at the nav's top level) becomes one group around them. Blume opens a group that's the sidebar's only top-level row by itself, but one beside the home page's row or another section starts closed off its own pages, so the codemod writes `collapsed: false` in its `meta.ts`. A `navigation.tabs` entry instead lifts its pages to the sidebar's top level, but there pages list above groups, so JTD's interleaved order breaks. Keep the group unless the user asks for a tab.\n- **The home page** (`index.md`, often `layout: home` and untitled) stays the content root's `index`. When the old nav never showed it, the codemod hides its row like any page outside the nav; a hidden home page stays in the sitemap, search, and `llms.txt`. Give it a `title` (Blume renders it as the H1, where JTD showed none) or `mode: frame` to keep the sidebar without one. Where the home page headed a JTD section, Blume's root index can't head a group: its children stay in their folders, and the codemod reports it.\n- **Moving a page changes what its relative images resolve against.** The codemod rebases the Markdown images it converted, and turns a relative raw HTML `src` into a root path, with a REVIEW to serve that file from `public/`.\n\n## URLs and redirects\n\nA Jekyll page's URL, for `dir/name.md`:\n\n| `permalink` (global) | `dir/name.md` | `dir/index.md` | Blume route |\n| --- | --- | --- | --- |\n| `pretty` | `/dir/name/` | `/dir/` | `/dir/name`, `/dir` |\n| unset (`date`), `none`, `ordinal` | `/dir/name.html` | `/dir/` | same |\n| a style ending in `/` / in `:output_ext` / neither | `/dir/name/` / `.html` / `/dir/name` (file `name.html`) | `/dir/` | same |\n\nA page's own `permalink:` wins, then one `defaults:` sets for its folder (the most specific `scope.path`), then the global style. The codemod applies all three, and with `--old` it checks each URL against the build: a page the build doesn't have at that URL gets a REVIEW and no `.html` redirect. Blume serves `<route>/index.html`, so `/dir/name/` keeps working on static hosts, and `blume dev` and `preview` redirect it to `/dir/name`.\n\n- **`.html` URLs** become one exact redirect each (a pattern can't strip a suffix). Never add one from `/dir/`, `/dir/index.html`, or `/index.html` (SKILL.md, Redirects).\n- **`redirect_from`** values become redirects as written (`/old` and `/old/` are different `from`s), keeping case (`/w/Main_Page`); one that is the page's own URL is skipped. **`redirect_to`** becomes a redirect from the page's route, and an empty page with it is deleted.\n- **Raw HTML** (`<a href>`, `<img src>`, `<video><source src>`) gains `deployment.base` on a root path, as a Markdown link does, so the root paths the codemod writes from relative links, `{{ site.baseurl }}`, `relative_url`, and `{% link %}` need nothing more. `blume validate` checks a raw `<a href>` against the routes and a media `src` against `public/`.\n- **Jekyll 3's `{% link %}`** doesn't add `baseurl` (Jekyll 4's does, so `{{ site.baseurl }}{% link … %}` doubled it there). The codemod drops both: Blume adds the base itself.\n\n## Content: what the codemod leaves\n\n**HTML includes** (`{% include x.html … %}`) are never spliced: `<include>` embeds a non-Markdown file as code. Replace each use and delete the partial when nothing uses it:\n\n- a figure → a Markdown image, or `<Frame caption=\"…\">` around it;\n- a YouTube iframe → `<YouTube url=\"…\" />`, inside `<Frame caption>` when the include printed a caption; a `width` → drop (report). A playlist URL (`/embed/videoseries?list=…`) works as written;\n- a row of embeds or code blocks in a flex `<div>` → `<Columns cols={2}>`;\n- a jQuery UI accordion (`$(\".accordion\").accordion()` over alternating question lines and answer `<div>`s) → `<Accordion>` with one `<AccordionItem title>` per question, the first `defaultOpen` as jQuery UI opened it; an `<details>` with Markdown in it → `<Expandable title>` (MDX wraps a `<summary>` and the text after it in one paragraph);\n- a note include (`note.html`, `callout.html`) → a directive.\n\n**Liquid logic** (`{% if %}`, `{% for %}`, `{% assign %}`, `{% capture %}`, a dynamic `{% include {{ var }} %}`, a parameter passed as a variable) → the output the old build shows: split a partial with `{% if include.x %}` into one partial per variant, write a `_data` loop's output once as Markdown, inline `{{ page.x }}`. A `{{ site.x }}` that isn't a plain value in `_config.yml` is left too. Liquid ran inside fenced code as well, unless wrapped in `{% raw %}`, so a REVIEW in a code block means the old page showed the rendered value.\n\n**CSS that changed content.** The codemod lists every `display: none` and `content: \"…\"` rule in `_sass/` and `assets/css/`. Find what each targets in the pages: content a rule hid wasn't shown, so delete it (and report the anchors that go with it); text a `::before`/`::after` added (`.label::before { content: \"New: \" }`) belongs in the converted element's text.\n\n**MDX hazards** the codemod lists in `.mdx` pages: a brace in prose (escape it `\\{` or put it in backticks), an indented code block (fence it), anything else the build names with a line. A partial with a hazard fails the build at the partial's own line (`blume check` warns `BLUME_MDX_SYNTAX` there first), and an unclosed `<img>` in a partial doesn't fail at all: it pulls the rest of the partial inside its parent element. The codemod self-closes the ones it sees, and `BLUME_MDX_UNCLOSED_ELEMENT` names the partial's line for any left.\n\n**A title in a partial:** a page that's only `{% include api/x.md %}` takes its heading from the partial's H1. Move it into the page's `title` (plain text: no `<kbd>` or backticks) and out of the partial, or Blume renders two.\n\n**Also by hand:** definition lists (`Term` + `: Definition` → a list or `<dl>`), `*[ABBR]:` abbreviations (delete), Font Awesome `<i class=\"fa…\">` icons (`<Icon icon>` with a Lucide name, or drop), and page scripts the codemod removed (an island, a `PageFooter` slot, or static content: two images instead of a before/after slider).\n\n### Callouts\n\nJTD has no built-in callout names: each site declares its own under `callouts:` with a `color` and an optional `title`, which JTD printed uppercase above every callout of that name. The codemod maps by the name's meaning, then by color:\n\n| JTD name (template color) | Blume |\n| --- | --- |\n| `note` (purple), `important` (blue) | `:::note`, `:::important` (an alias of note) |\n| `tip`, `hint` | `:::tip` |\n| `info`, `highlight` (yellow, untitled) | `:::info` |\n| `warning`, `attention`; `caution` | `:::warning`; `:::caution` (an alias of warning) |\n| `danger`; `error` | `:::danger`; `:::error` (an alias of danger) |\n| `new`, `success` (green) | `:::success` |\n| any other name | by its color: blue → `info`, green → `success`, yellow → `warning`, red → `danger`, purple or grey → `note` |\n\nBlume shows a type's icon, not its name, so a configured `title` that only names the directive is dropped and one that says more is kept (`new` titled \"New\" → `:::success[New]`). `{: .note-title }` takes its title from the blockquote's first paragraph. A callout class on a list, table, or code block rendered no box in JTD (it styles only paragraphs and blockquotes), so it's dropped. The codemod applies each IAL to the block Kramdown did: a list or blockquote that follows a paragraph without a blank line is its own block, and an IAL with blank lines on both sides applies to the next block. An IAL indented under a list item sat inside the item, where Kramdown sometimes boxed the item's paragraph: those are reported, so check the old page and write the callout inside the item by hand.\n\n## Front matter\n\nThe codemod maps these, and reports every other key Blume's strict schema rejects:\n\n| Jekyll / JTD | Blume |\n| --- | --- |\n| `title` + body H1 | `title` = the H1; a different old title → `sidebar.label` and `seo.title` |\n| `description`, or `summary` (JTD showed it in child lists) | `description`. JTD only put it in `<meta>`; Blume shows it under the title (report) |\n| `layout: default` / `page` / `home` / `post` / `about` | removed |\n| `layout: minimal`, `nav_enabled: false` | `mode: center` |\n| a custom `layout` from `_layouts/` | reported: pick a `mode` or a custom `.astro` page |\n| JTD nav keys, `permalink`, `redirect_from`, `redirect_to` | removed (Navigation, URLs) |\n| `search_exclude: true` | `search: { exclude: true }` |\n| `last_modified_date` | `lastModified` (ISO date) |\n| `published: false` | `draft: true` |\n| `image` (a string, or `{ path }`), `canonical_url` | `seo.image`, `seo.canonical` |\n| jekyll-seo-tag's `seo:` `type`/`name`/`links` | removed |\n| `sitemap: false` | reported: `seo: { noindex: true }` if it shouldn't be indexed, else delete |\n| `author`, `date`, `lang`, `tags`, `categories`, `excerpt`, `toc` | reported: delete (`tags` → `search.tags` if a hosted search filters on them) |\n\n## Theme, layouts, and assets\n\n- **`_sass/custom/custom.scss`** → a project-root `theme.css` with only the rules whose markup still exists (figure classes, flex rows): JTD selectors (`.main-content`, `.site-nav`, `.site-title`) match nothing in Blume. Turn hard-coded colors into Blume's tokens.\n- **Theme hook includes** (the codemod lists them): `head_custom.html` (favicons → `public/`, `<meta>` → `seo.metatags`, analytics → adapters, icon-font CSS → drop), `header_custom.html` (an announcement → `banner`), `footer_custom.html` (Config), `title.html` (→ `logo`), `nav_footer_custom.html`, `search_placeholder_custom.html`, `toc_heading_custom.html`, `mermaid_config.js`, `js/custom.js`, `lunr/*` → drop and report what each did.\n- **`_layouts/`** and `.html` pages with front matter → `.mdx` (with `mode: custom` for a landing page) or a custom `.astro` page. `404.html`/`404.md` → delete (Blume ships one).\n- **Assets:** a folder the old site served by root path (`/media/…`, `/assets/images/…`) moves whole to `public/` at the same path. Keep files no page references: other sites may link them. List them, and prune only once the user confirms. JTD's own `assets/css` and `assets/js` go.\n- **Root files** (`favicon.ico`, `apple-touch-icon.png`, `site.webmanifest`, `.well-known/`) → `public/`. A `robots.txt` replaces Blume's generated one: keep it only if it says something Blume's doesn't. `CNAME` and `.nojekyll` aren't needed with a GitHub Actions deploy.\n\n## Collections and posts\n\n- **A JTD collection** (`collections: { docs: { output: true, permalink: \"/:collection/:path/\" } }` plus `just_the_docs.collections.docs.name`): rename `_docs/` → `docs/` so the URLs stay `/docs/…`, then run the codemod; give the folder a `meta.ts` `title` from `name`. `nav_fold: true` → `collapsed: true`. A collection document without `title` took its filename as one: write it in.\n- **`_posts/`** → `type: blog` pages (`docs/advanced/blog.mdx`), with `slug` pinned to the old dated URL (`/:categories/:year/:month/:day/:title.html` by default, so also an `.html` redirect) or redirected; `{% post_url %}` → the post's route; jekyll-feed's `/feed.xml` → a redirect to `/blog/rss.xml`.\n\n## Plugins\n\n| Plugin | Blume |\n| --- | --- |\n| `jekyll-redirect-from` | `redirects` (the codemod) |\n| `jekyll-seo-tag`, `jekyll-sitemap` | built in (the sitemap needs `deployment.site`) |\n| `jekyll-relative-links` | built in: the codemod turns `.md` links into routes |\n| `jekyll-include-cache` (`include_cached`) | same as `include` |\n| `jekyll-remote-theme`, `jekyll-default-layout`, `jekyll-optional-front-matter`, `jekyll-titles-from-headings`, `jekyll-readme-index` | delete; the old build shows the pages and titles they made |\n| `jekyll-github-metadata` (`site.github.*`) | inline the values; `repository` → `github` |\n| `jekyll-last-modified-at` | `lastModified: \"git\"` |\n| `jemoji` (`:smile:`), `jekyll-mentions` (`@user`), `jekyll-gist` | paste the emoji; write the links; a link or a fence |\n| `jekyll-paginate`, `jekyll-coffeescript`, `jekyll-sass-converter`, `jekyll-compose` | drop |\n| anything else (`_plugins/*.rb` included) | read what it does; report it |\n\n## Plain Jekyll and other themes: manual\n\nRun the codemod without `--old` (or with it, for the URLs): its content and link passes apply to any Jekyll site. The navigation source differs, and there's no tested mapping for it:\n\n- **Plain Jekyll / minima:** `header_pages` (a list of files) or no nav. Lay out folders by topic and report the new sidebar.\n- **Minimal Mistakes** (docs layout): `_data/navigation.yml` holds the sidebar (a key such as `docs:` listing `title` + `children: [{ title, url }]`), and pages opt in with `sidebar: { nav: \"docs\" }`. Convert it like a config-declared nav (SKILL.md). Notices `{: .notice }` and `.notice--primary`/`--info`/`--warning`/`--success`/`--danger` → pass those names with `--callouts` and map the types by hand; `toc: true` and `toc_*` → delete.\n- **Documentation Theme for Jekyll:** `_data/sidebars/*.yml` (`entries` → `folders` → `folderitems`, then `subfolders` → `subfolderitems`), with `sidebar: <file>` and a flat `permalink: name.html` on every page. `{% include note.html content=\"…\" %}` (also `tip`, `warning`, `important`, and `callout.html` with a `type`) → directives; `summary` → `description`; `keywords` → `search.keywords`; `last_updated` → `lastModified`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts` (a Ruby site has none: scaffold it), Node 22.19+, `.gitignore` for `node_modules/`, `.blume/`, `dist/`, `.env.local`.\n- **CI:** replace `bundle exec jekyll build` and the Pages starter workflow with the Blume one (https://useblume.dev/guides/markdown-docs-github-pages). Its `npm ci` needs a lockfile, so commit the `package-lock.json` from `npm install` in the same change. A branch-deployed Pages site moves its source to GitHub Actions in the repo settings, where the custom domain also stays: list both as manual steps. Dependabot's `bundler` entry → `npm`. Scripts that wrapped Jekyll (`serve.bat`, `Rakefile`) → delete or repoint.\n- **Delete** `_config.yml`, `Gemfile`, `Gemfile.lock`, `_layouts/`, `_sass/`, theme hook and HTML includes, JTD's `assets/` files, `_site/`, `.jekyll-cache/`, and `vendor/bundle/`, once everything they declared is harvested. Keep `_includes/` partials pages still `<include>`, and `jekyll-migration.json`.\n- **Grep the whole repo** for moved page paths and for `jekyll` (CODEOWNERS, link checkers, a README \"build the docs\" section).\n\n## Leftover check\n\nRerun until it prints only code samples, `{{name}}` variables and include props you defined, and relative links you meant:\n\n```bash\ngrep -rnE '\\{:[ .#a-z:]|\\{%|\\{\\{|^\\*\\[|^: |markdown=\"|<i class=\"fa|\\.html[)#\"]|\\]\\(\\.\\.?/[^).]*\\)|\\]\\(/[^)]*/(#[^)]*)?\\)' \\\n  --include='*.md' --include='*.mdx' --exclude-dir={node_modules,vendor,_site,.blume,dist} .\n```\n\n## Dropped — report these\n\nCallout title labels that only named the type, unconfigured or malformed callout IALs left plain, the button and label styling the codemod dropped, `nav_external_links` moved above the sidebar, `back_to_top`, other `footer_content` text and links inside the copyright, theme hooks, custom color schemes beyond the accent, `_sass` rules with no Blume target, content a CSS rule hid, site-wide code line numbers, deeper TOC levels, `UA-` analytics ids, `sitemap: false`, abbreviations, definition-list styling, Liquid logic and `_data` loops now frozen as content, embed widths, page scripts and widgets (sliders, accordions) not rebuilt, icon-font icons with no Lucide match, accidentally published files (`LICENSE`, `.bat`), and every plugin removed without an equivalent.\n",".agents/skills/blume-migrate/references/mdbook.md":"# mdBook → Blume\n\nmdBook is the Rust project's book generator: a `book.toml`, a hand-written `SUMMARY.md` that is the whole table of contents, and chapters in `src/` rendered by pulldown-cmark after preprocessors rewrite them (the built-in `links` expands `{{#include}}`; plugins like mdbook-admonish and mdbook-mermaid add the rest). Watch for five traps:\n\n- **Every URL ends in `.html`** (`src/guide/setup.md` → `/guide/setup.html`, a `README.md` → `index.html`), so every chapter that isn't an index needs an exact redirect.\n- **`SUMMARY.md` is the only navigation, and its nesting rarely exists on disk.** A parent chapter is a page _and_ a group, and its file sits beside its children's folder (`guide.md` + `guide/`) or flat next to them.\n- **`{{#include}}` pulls code from anywhere in the repo, by `ANCHOR` name or line range.** Blume's `<include>` takes whole files from inside the content root only, so code the crate compiles goes through generated excerpts (Includes). Anchors that no longer exist rendered as empty blocks on the old site, with no warning, so expect several.\n- **Fences carry Rust-only syntax.** Hidden `# ` lines and attributes like `rust,ignore` show up as code or kill highlighting if left alone.\n- **The page heading is the body H1, but the sidebar label and `<title>` are the `SUMMARY.md` text**, and the two often differ.\n\nScope: mdBook 0.4.x and 0.5.x, checked against 0.4.48, 0.4.52, and 0.5.4, mdbook-admonish 1.19.0, mdbook-mermaid 0.15.0, and an end-to-end migration of the Linera manual (39 chapters, mdBook 0.4.48). The syntax of mdbook-katex, -toc, -tabs, and -alerts comes from their docs; it wasn't run.\n\n## Detect\n\n- **`book.toml`**, with `[book]` and usually `[output.html]` tables: only mdBook uses it. It sits at a standalone book's root or in a folder of a code repo (`docs/`, `book/`, `doc/`, `guide/`, `mdbook/`). The chapters live in `[book] src` (default `src/`), which can point outside that folder (`src = \"../docs\"`).\n- **`SUMMARY.md`** in that `src`. GitBook and HonKit use `SUMMARY.md` too, but with `.gitbook.yaml` or `book.json`, never `book.toml`.\n- CI that runs `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, or `cargo install mdbook mdbook-<plugin>`. The workflow that deploys may live in another repo (one that checks the code repo out as a submodule).\n- **Fingerprint the version** from the CI pin. 0.5 removed `curly-quotes`, `multilingual`, `google-analytics`, and `copy-fonts`, rejects unknown `book.toml` keys, and fails on plugins built for 0.4 (mdbook-admonish 1.19.0 and mdbook-mermaid 0.15.0 both do), so most books with plugins still build with 0.4.x.\n\n**Where the Blume project goes.** Beside `book.toml`, the folder `mdbook build` ran in, with `content.root` set to the book's `src` (Blume's default is `docs`, so set it unless `src = \"docs\"`). That keeps `package.json`, `public/`, and `dist/` out of a Rust repo's root. mdBook has no manifest: scaffold `package.json` there (SKILL.md step 6). **A `src` outside that folder (`\"../docs\"`) works**, edit links included, and a `meta.ts` there imports `defineMeta` from `blume` as usual. That import needs a Blume release after 2.1.3: earlier ones resolved `blume` only from the meta file's own folder up, so it failed with `BLUME_META_LOAD_FAILED … Cannot find module 'blume'`. On those, export a plain object instead (`export default { title: \"Developers\", display: \"flat\", pages: […] };`).\n\n## Build the old book first\n\nBefore changing anything, build the book with the version CI pins and its plugins, into a folder outside the repo. Prebuilt binaries of mdBook and most plugins are on their GitHub releases pages (mdbook-admonish has none for Apple Silicon: `cargo install --root <scratch> mdbook-admonish@<version>`); install them in a scratch folder, not globally.\n\n```bash\nmdbook build <book-root> -d /abs/path/old-book\nMDBOOK_OUTPUT='{\"markdown\": {}}' mdbook build <book-root> -d /abs/path/old-md\n(cd /abs/path/old-book && find . -name '*.html' ! -name print.html ! -name toc.html ! -name 404.html | sed 's|^\\.||' | sort) > old-urls.txt\n```\n\n- `old-book` is the old site: its `.html` files are the old URLs, its headings carry the old ids, and its pages show what readers saw. With two or more `[output.*]` tables (`[output.linkcheck]` counts), the HTML lands in `old-book/html/`: use that folder wherever this file says `old-book`.\n- `old-md` is each chapter as Markdown after `{{#include}}` and the other helpers ran (admonish blocks stay as written): what every include actually showed.\n- Under 0.4 a missing plugin only warns, and its blocks render as code; the URLs are still right. Under 0.5 it fails unless `optional = true`.\n\n## Config: `book.toml` → `blume.config.ts`\n\nMap only what's set. CI can override keys with `MDBOOK_BOOK__…`/`MDBOOK_OUTPUT__…` environment variables, so read those too.\n\n| `book.toml` | Blume |\n| --- | --- |\n| `[book] title` | `title`. Unset, mdBook showed no title: take the first chapter's H1, or ask |\n| `description` | `description` |\n| `src` | `content.root` |\n| `language` (not `en`) | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`); `text-direction = \"rtl\"` → that locale's `dir: \"rtl\"` |\n| `authors`, `multilingual`, `[rust] edition`, `[build] build-dir`, `create-missing`, `extra-watch-dirs` | drop. With `create-missing` (on by default) mdBook created an empty page for a `SUMMARY.md` entry whose file was missing: check the old build |\n| `use-default-preprocessors = false` | without `[preprocessor.links]`/`[preprocessor.index]`, `{{#…}}` stayed literal and `README.md` stayed `README.html`: read the old build |\n| `[output.html] site-url = \"/repo/\"` | `deployment: { base: \"/repo\" }`. It's a path, not an origin |\n| `cname` | `deployment: { site: \"https://<cname>\" }`; GitHub Pages isn't auto-detected. A project site with no domain → `{ site: \"https://<owner>.github.io\", base: \"/<repo>\" }` |\n| `git-repository-url` on GitHub | `github: { owner, repo }`. Elsewhere → `navigation.actions` (report) |\n| `edit-url-template` | `github.branch` from the segment after `edit/` or `blob/`. Set `github.dir` to the Blume project's path in the repo (`git rev-parse --show-prefix`), not the template's prefix: mdBook fills `{path}` with `<src>/<file>`, so a `src` outside the book root gave the old site broken edit links (`…/edit/main/../docs/x.md`) |\n| `default-theme`, `preferred-dark-theme` | mdBook switches between them on the reader's system preference. Both dark (`coal`, `navy`, `ayu`) → `theme.mode: \"dark\"`; a light one plus a dark one → omit. A theme switcher deleted from `theme/index.hbs`, or a stylesheet that repaints the default theme, forces one look: read the CSS, not the name |\n| `additional-css` | a root `theme.css` with only the rules whose markup still exists (`.content`, `.sidebar`, `.chapter`, `--links`, `--sidebar-*` match nothing). Read brand colors and fonts into `theme.accent`, `theme.background`, and `theme.fonts`; Blume already defaults to Inter and IBM Plex Mono. Delete `mdbook-admonish.css` |\n| `additional-js` | delete the mermaid, admonish, and KaTeX assets. A gtag loader → `googleAnalytics({ id })` from `blume/analytics`; other scripts → `script()` or report |\n| a `theme/` folder | diff it against `mdbook init --theme` from the same version first: stock files carry nothing to port, and the stock `favicon.png`/`favicon.svg` are mdBook's own logo (report, don't copy). Custom ones: `<head>` meta tags → `seo.metatags`; analytics → adapters; a favicon → `public/`; fonts → `theme.fonts`; the rest drops (report) |\n| `google-analytics` (0.4) | `googleAnalytics({ id })` for a `G-` id; report a `UA-` id |\n| `mathjax-support` | Math (Other syntax) |\n| `smart-punctuation` / `curly-quotes` | Blume always converts quotes and dashes. Where the old site had it off (0.4's default), `--flag` in prose becomes an en dash: put flags in backticks |\n| `no-section-label` | nothing. Without it, mdBook numbered the chapters (`1.`, `2.3.`) in the sidebar, and Blume can't: report it, and fix prose that cites \"chapter 4\" |\n| `input-404`, `src/404.md` | delete the page; Blume ships a 404 (`pages/404.astro` to customize) |\n| `[output.html.search]` | built in: drop. `search.chapter.\"<path>\" = { enable = false }` → `search: { exclude: true }` on those pages |\n| `[output.html.fold]` | Navigation |\n| `[output.html.redirect]` | URLs and redirects |\n| `[output.html.playground]`, `[output.html.code.hidelines]` | Code blocks |\n| `[output.html.print]`, `hash-files`, `copy-fonts`, `sidebar-header-nav`, `git-repository-icon`, `definition-lists`, `admonitions`, `live-reload-endpoint`, `[output.markdown]` | drop |\n| `[output.linkcheck]`, `[output.linkcheck2]` | `blume validate --strict` (`--external` for web links) |\n| other backends (EPUB, PDF) | `export: { pdf: true }` prints one page at a time; a whole-book PDF or EPUB has no equivalent (report) |\n\n## Preprocessors\n\n| `[preprocessor.X]` | Blume |\n| --- | --- |\n| `links` (built in) | Includes |\n| `index` (built in) | rename every `README.md` chapter to `index.md` |\n| `admonish` | Admonitions |\n| `mermaid` | ` ```mermaid ` works as written in `.mdx`; delete `mermaid.min.js` and `mermaid-init.js` (theme settings in `mermaid-init.js` drop) |\n| `katex` | `$…$` → `$$…$$` inside its sentence; `$$` blocks stay; `\\$` → `$`. Custom delimiters → convert from those. A `macros` file has no equivalent: expand the macros |\n| `toc` | delete the `<!-- toc -->` marker (or the configured `marker`): the outline rail replaces it |\n| `pagetoc`, `open-on-gh` | built in (outline rail; `github` edit links): delete |\n| `alerts` | GitHub alerts (Admonitions) |\n| `tabs` | `{{#tabs}}` / `{{#tab name=\"X\"}}` … `{{#endtab}}` / `{{#endtabs}}` → `<Tabs>` / `<Tab title=\"X\">`. `global=\"…\"` drops: Blume syncs same-titled tabs page-wide |\n| `i18n-helpers` (gettext, `po/*.po`) | i18n (Repo integration) |\n| `cmdrun` (`<!-- cmdrun … -->`), `template`, custom ones (`command = \"cargo run …\"`) | diff `old-md/` against `src/` to see what each one wrote, then freeze that output or move the generator into a prebuild script (report) |\n| `linkcheck` | `blume validate --strict` |\n| other diagram plugins (graphviz, plantuml, svgbob) | render each diagram to an SVG once and commit it, or redraw it in mermaid (report) |\n| anything else | read what it does; report it |\n\n## Navigation: `SUMMARY.md` → folders and `meta.ts`\n\n### How mdBook reads `SUMMARY.md`\n\n- A `# Summary` first line is ignored. **Prefix chapters** are bare `[Title](file.md)` lines before the first list, and can't nest. **Part titles** are `# Title` headings (h1 only) over the numbered chapters that follow: plain headings, not links. **Numbered chapters** are list items (`-`, `*`, or `1.`) that nest by indentation. **Suffix chapters** are bare links after the last list. A **draft** is `[Title]()` (a greyed-out entry, no page). A **separator** is a `---` line; it can also split one part's list, leaving chapters after it under no visible heading.\n- Only listed chapters render. Any other `.md` under `src/` isn't published, but every non-Markdown file there is copied to the site at its own path.\n- A chapter's URL is its path with `.html`; a `README.md` (any case) becomes `index.html`. **The first chapter is also written to `/index.html`.** Its sidebar label and `<title>` are the `SUMMARY.md` text (plus ` - <book title>`); the page shows its own H1.\n\n### Put every chapter in place\n\nKeep each chapter's path, so its route is the old URL minus `.html`, and keep a table of old URL → file → route as you go: it becomes `redirects`.\n\n- **Every `README.md` chapter → `index.md`.** Its route is the folder's, which `x/index.html` already served. Left as is, Blume publishes it at `/x/README`.\n- **A parent chapter beside its children's folder** (`guide.md` + `guide/`): `git mv guide.md guide/index.md`. The route stays `/guide` and the folder row links to it; left in place, `guide.md` is a separate page beside an unlinked \"Guide\" group. The page moved a level deeper: rewrite its relative links, images, and include paths, and every link to `guide.md`.\n- **Children flat beside their parent** (`ch03-00-concepts.md`, `ch03-01-variables.md`): a `(group)/` folder holding the parent as `index.md` with `slug: ch03-00-concepts`, plus the children. The group adds no URL segment, the `slug` keeps the parent's route, and the group row links to it. Rebase relative paths in every moved file.\n- **The first chapter**, when it isn't the root `README.md`: move it to the content root's `index.md` and redirect its own URL to `/` (`{ from: \"/introduction.html\", to: \"/\" }`), since mdBook served it at both.\n- **Ordering prefixes.** Blume drops a leading `\\d+[-_.]` from every segment, keeping versions (`1.2.0`) and dates (`2024-01-05`, `12-05-2022`, `2024-01`) whole (`00-overview.md` → `/overview`). Every old URL is redirected anyway, so let Blume drop them (they keep ordering the sidebar) and point those redirects at the new routes, or pin `slug` on each page to keep the old path. Say which.\n- **Files `SUMMARY.md` doesn't list** were never published: a contributors' `README.md` beside the chapters, notes, Markdown that `{{#include}}` pulls in. Prefix them with `_`, move them out of the content root, or add them to `content.exclude` (it adds to the defaults `\"**/_*\"` and `\"**/.*\"`). Report them.\n- **A chapter whose body is a `<meta http-equiv=\"refresh\" content=\"0; url=…\">`** is a redirect, not a page to keep: add a redirect from its `.html` URL to the target, a `navigation.featured` link in place of its sidebar entry, and delete it (mdBook's `print.html` ran the same tag, so it sent the whole print page away too). Report it.\n- Delete `SUMMARY.md` once folders and `meta.ts` carry its structure: it would publish as a page.\n- **Check that git sees the new files.** A repo `.gitignore` can cover the content root (pages added before the rule stay tracked), and then every `meta.ts` and partial you create is silently left out of `git add`. Run `git check-ignore -v <content root>/meta.ts` after writing the first one; if it prints a rule, fix that rule or report that each new file needs `git add -f`.\n\n### Parts, order, labels, and folding\n\n- **Display:** set `navigation: { sidebar: { display: \"group\" } }`. Parent chapters become collapsible rows, like mdBook's tree, and a group keeps leaf chapters between its subgroups in `meta.ts` order (SKILL.md: `flat` can't interleave them).\n- **Part titles** → a folder per part, with `title: \"<part title>\"` and `display: \"flat\"` in its `meta.ts`: a heading, not a row. When a part's chapters already share a folder (`developers/`), that folder is the part; otherwise use a `(part)/` folder at the content root, which changes no URL. Chapters after a separator inside a part get the folder they're already in (`appendix/` reads \"Appendix\"), or the part's heading.\n- **Order** → a `meta.ts` `pages` array in each folder, every child in `SUMMARY.md` order. The root `meta.ts` orders the parts.\n- **Prefix chapters** stay loose pages at the content root, which Blume lists first, as mdBook did.\n- **What files can't express:** at the top of the sidebar, loose pages always list above folders. Suffix chapters, and leaf chapters between parent chapters in a book with no parts, rise above them. Put each in a `(group)/` folder (it adds a heading), or accept the order. When many chapters are affected, an explicit `navigation.sidebar` keeps `SUMMARY.md`'s order exactly: a route string per leaf chapter, `{ label, root: \"<route>\", items }` per parent chapter, `{ label, display: \"flat\", items }` per part. It replaces the generated tree, so it needs an entry per new page, as `SUMMARY.md` did. Report which you chose.\n- **Labels:** `title` is the body H1, what the page showed. `sidebar.label` is the `SUMMARY.md` text where it differs, what the sidebar and `<title>` showed. A page with no H1 takes the `SUMMARY.md` text as `title`. Give each parent chapter's folder `title: \"<its SUMMARY.md text>\"` in `meta.ts`, or its row shows the humanized folder name. Strip Markdown from `SUMMARY.md` text: backticks render literally in `title`.\n- A parent's `index` page also lists as its group's first row, so the label shows twice. To show it once, as mdBook did, set `sidebar.hidden: true` on the index: it stays in `sitemap.xml` and search because the group row still links to it. If its `title` differs from the folder's `meta.ts` title, set its `sidebar.label` to the folder title, or `BLUME_NAV_INDEX_TITLE_MISMATCH` fails `validate --strict`.\n- **`[output.html.fold]`:** with `enable = true`, sections below `level` started closed (0 closes all), which is `group` mode's default; add `collapsed: false` to the folders mdBook opened. Without folding, mdBook showed every section open: add `collapsed: false` to every folder's `meta.ts`.\n- **Drafts and separators** have no equivalent: drop them, and report the drafts as planned chapters.\n\n## Content\n\n### `.md` or `.mdx`\n\nRename a page to `.mdx` when it ends up with a callout, mermaid, math, tabs, or another component. Those are MDX-only: in `.md` they don't render, and the build stays green (a `:::` callout only warns `BLUME_MD_DIRECTIVE`). Tables, footnotes, strikethrough, task lists, `[#id]` heading ids, `<include>`, `{{variables}}`, and raw HTML work in both (`{#id}` only in `.md`). Fix what fails `.mdx` in each renamed page: HTML comments → `{/* … */}` on one line, unclosed `<br>`/`<img>`, `<https://…>` autolinks, and `{`, `<` in prose (`Vec<T>` outside backticks).\n\n### Includes and the other `{{#…}}` helpers\n\nmdBook expands these as text before parsing, so they work inside fences, inline code, and link URLs, and across a line break (`{{#include` at a line's end, the path on the next, as Prettier wraps it). A path resolves from the including file.\n\n| mdBook | Blume |\n| --- | --- |\n| a fence holding only `{{#include path}}`, the whole file, inside the content root | the whole fence → `<include lang=\"rust\">path</include>` on its own line (the fence's language; its attributes drop) |\n| a fence holding only `{{#include path:anchor}}`, `path:N`, `path:N:M`, `path:N:`, or `path::M`, or any target outside the content root | an excerpt (below), included the same way |\n| a fence mixing includes with hand-written lines | one excerpt that joins `{ \"text\": \"…\" }` parts and selections in order, so the block stays generated |\n| `{{#rustdoc_include path:sel}}` | an excerpt of `path:sel`: the selected lines only. The rest of the file sat behind mdBook's expand toggle, which has no equivalent (report) |\n| `{{#playground path attrs}}` | `<include lang=\"rust\">` of the file (an excerpt when it's outside the root); no Run button (report) |\n| `{{#include x.md}}` on its own line | `<include>/_snippets/x.md</include>` (a leading `/` is the content root): a partial must sit under a `_` folder or name, or it publishes as a page. Images in a partial resolve from the partial's own folder (mdBook resolved them from the including chapter), so fix their paths |\n| a one-line file included inline (`{{#include ../VERSION}}` in a URL or a command) | `{{VERSION}}`, with `variables: { VERSION: readFileSync(new URL(\"../VERSION\", import.meta.url), \"utf8\").trim() }` in `blume.config.ts` (`import { readFileSync } from \"node:fs\"`). Variables are replaced in links, code, and inline code too |\n| `{{#title X}}` | `seo.title: X` (Blume appends the site title, as for every page) |\n| `\\{{#include x}}` (often in a fence that documents mdBook) | drop the backslash, which mdBook removed even inside code. `{{#include x}}` stays literal in code and in `.md`; in `.mdx` prose a `{` fails the build, so put it in inline code |\n\n**Excerpts.** `<include>` fails the build on a target outside the content root (`BLUME_INCLUDE_OUTSIDE_ROOT`) and can't select lines. Pasting each excerpt into its page would freeze code that cargo still compiles and tests, so generate them before every run with the bundled `include-excerpts.mjs`:\n\n1. Copy `<skill>/scripts/include-excerpts.mjs` beside `blume.config.ts`. Its header documents the manifest.\n2. Write `excerpts.json` there: `out` (`<content root>/_excerpts`, relative to the manifest; Blume doesn't publish a `_` folder), and one entry per excerpt, keyed by its file name under `out`. An include's argument works as written once its path is relative to the manifest's folder (`\"counter/abi.rs\": \"../examples/counter/src/lib.rs:contract_abi\"`): the script selects anchors and line ranges exactly as mdBook did. A mixed fence is an array: `[{ \"text\": \"impl Contract for Token {\" }, \"../src/contract.rs:transfer\", { \"text\": \"}\" }]`.\n3. Run `node include-excerpts.mjs`. It checks every entry first and lists **every** missing file, missing anchor, and line range past the end of its file in one pass, then writes nothing. A clean run writes the excerpts and deletes the ones it wrote earlier that the manifest dropped (never a file it didn't write).\n4. Replace each include with `<include lang=\"rust\">/_excerpts/counter/abi.rs</include>`.\n5. Run it first in `package.json` (`\"dev\": \"node include-excerpts.mjs && blume dev\"`, and the same for `build` and a `validate` script), and gitignore `<content root>/_excerpts/`. A host's build command must be `npm run build`, not `blume build`. Rerun it after editing a source file: `blume dev` doesn't watch them.\n\n**Missing anchors.** For each anchor the script reports (the old site showed an empty block, or only a mixed block's hand-written lines), find where the code went (`git log -S \"ANCHOR: <name>\"`, or search the crate for the item), then:\n\n- **When the team owns the source, re-add the `// ANCHOR: <name>` and `// ANCHOR_END: <name>` comments around the code there.** The entry keeps working as written, and the excerpt follows the code as it changes.\n- Otherwise, point the entry at a line range (`\"…/lib.rs:14:23\"`). It keeps selecting those line numbers when the file changes, without an error, so say so in the report.\n- Drop the block, with its lead-in sentence, when the code is gone.\n\nReport each one and what you chose. Code that now fills a formerly empty block may contradict the prose around it: list what you notice.\n\n### Code blocks\n\n| mdBook | Blume |\n| --- | --- |\n| ` ```rust,ignore `, `rust,no_run,noplayground`, `rust,should_panic`, `rust,compile_fail`, `rust,edition2021`, `rust,editable`, `rust,mdbook-runnable`, `rust,hidelines=…` | ` ```rust ` (a comma-glued language doesn't highlight, and warns `BLUME_UNKNOWN_CODE_LANGUAGE`; written with a space, ` ```rust ignore `, the keyword does nothing and warns `BLUME_CODE_FENCE_OPTION`) |\n| ` ```ignore `, or no language: rustdoc tests an unlabeled block as Rust, so books label diagrams and shell lines `ignore` | ` ```text `, or the real language |\n| **hidden lines** in a Rust block: first non-space character `#`, then a space or the end of the line | delete the line, and a blank line it leaves at the top of the block: readers saw it only through the eye toggle, which has no equivalent. `##x` shows as `#x`; `#[derive]` and `#!` lines are code. Lines with a `[output.html.code.hidelines]` prefix (`python = \"~\"`), or a fence's `hidelines=<prefix>`, go the same way |\n| the play button, `editable` | static code (report) |\n| `mdbook test` | nothing compiles Blume's code blocks. Excerpts stay tested by cargo; hand-written blocks that `mdbook test` compiled aren't any more (report; drop the CI step and whatever it built only for that) |\n\n### Admonitions\n\n**mdbook-admonish** writes ` ```admonish <type> ` with options (`title=\"…\"`, `collapsible=true`, `id`, `class`), the older `admonish <type> \"Title\"`, or `~~~`/four-backtick outer fences around inner code. Each becomes a callout in `.mdx`, with a longer `::::` outer fence around a nested one:\n\n| admonish type (aliases) | Blume |\n| --- | --- |\n| `note`, `example` | `:::note` |\n| `info` (`todo`), `abstract` (`summary`, `tldr`), `question` (`help`, `faq`) | `:::info` |\n| `tip` (`hint`, `important`) | `:::tip` |\n| `success` (`check`, `done`) | `:::success` |\n| `warning` (`caution`, `attention`) | `:::warning` |\n| `danger` (`error`), `failure` (`fail`, `missing`), `bug` | `:::danger` |\n| `quote` (`cite`) | a blockquote |\n| **any other word** (`admonish warn`) | `:::note`: admonish rendered it as a note titled \"Note\". Don't keep the word: Blume reads `:::warn` as a warning. Report it, with the type the author probably meant |\n\n- `title=\"T\"` → `:::type[T]`; `title=\"\"` → no title. An untitled block showed the word it was written with as its title bar (`Example`, `Hint`, `TL;DR`), and an untitled Blume callout shows none: where that word isn't the Blume type's name, keep it as the title (`:::note[Example]`).\n- `collapsible=true` → `<Expandable title=\"T\">` (the type's name when untitled); its color drops.\n- Custom directives (`[[preprocessor.admonish.custom]]`) → the closest callout, with the label as title.\n- Each block had an anchor (`#admonition-<title>`, `-1` for repeats). Links to one fail `blume validate --strict`: point them at the nearest heading or drop the fragment.\n\n**GitHub alerts** (`> [!NOTE]`, built into 0.5 and the `alerts` plugin) render as callouts in `.mdx` as written: `NOTE` and `IMPORTANT` as a note, `TIP` a tip, `WARNING` a warning, `CAUTION` a danger callout. In `.md` they stay a blockquote with a literal `[!NOTE]` and warn `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. A 0.4 book without the plugin showed them as plain quotes: rendering them changes the page (report).\n\n### Other syntax\n\n| mdBook | Blume |\n| --- | --- |\n| `## Heading {#id}` | the same in `.md`; `[#id]` in `.mdx` |\n| `## Heading { #id .class }` | `{#id}` or `[#id]`; classes drop. Left as is, the spaced form stays in the heading text and makes a garbage id; `BLUME_MD_CURLY_ANCHOR` flags it only when no class follows the id (in `.mdx` it fails the build) |\n| footnotes, tables, strikethrough, task lists | unchanged |\n| definition lists (`term` then `: definition`, on by default in 0.5) | a list or table, or raw `<dl>`; Blume renders them as one paragraph |\n| MathJax (`mathjax-support`): `\\\\( … \\\\)`, `\\\\[ … \\\\]` | `$$…$$` inside the sentence, or on lines of its own for a block, in `.mdx`. Undo the Markdown escapes mdBook needed inside the math (`\\\\\\\\` for a LaTeX `\\\\`, any `\\_` or `\\*`): `$$` math is read verbatim |\n| `<i class=\"fa fa-x\"></i>` | `<Icon icon=\"<lucide>\" />` in `.mdx`, or delete (the icon map in `references/mintlify.md`) |\n| `class=\"left\"`/`\"right\"` floats; `class=\"hidden\"` | drop the class; delete hidden elements (report) |\n\n### Links and images\n\n- mdBook only rewrote `x.md` links to `x.html`. Blume resolves a relative `.md` link to the page that file publishes: keep them, and fix the ones into or out of every moved file.\n- Rewrite `.html` links (`installation.html#cargo`) to their `.md` files: Blume serves no `.html` URLs, so they'd depend on the redirects. `blume validate` reports each one left (`BLUME_BROKEN_ASSET`, with the page's route).\n- **A link to `README.md` was already dead:** mdBook wrote `README.html`, a page it never creates. Point it at the renamed `index.md`.\n- Root-relative links (`/guide/x.md`) land on the page that file publishes, read from the content root, like relative ones.\n- Images relative to the page work as written. So does a linked download (a PDF, an archive) that mdBook copied from `src/`: Blume publishes a file a relative link names with the page, at a new URL. Move it to `public/` at the same path only if its old URL is linked from elsewhere.\n\n## URLs and redirects\n\n| Source file | mdBook URL | Blume route |\n| --- | --- | --- |\n| `src/guide/setup.md` | `/guide/setup.html` | `/guide/setup` |\n| `src/guide/README.md` | `/guide/index.html` | `/guide` |\n| the first chapter, `src/intro.md` | `/intro.html` and `/index.html` | `/` once moved to `index.md` |\n| `src/01-basics.md` | `/01-basics.html` | `/basics`, or `slug: 01-basics` |\n\n- **One exact redirect per chapter that isn't an index**, `{ from: \"/guide/setup.html\", to: \"/guide/setup\" }`, built from `old-urls.txt` and your table: a pattern can't strip `.html`. Skip `/index.html` and `/x/index.html`: static hosts serve Blume's `x/index.html` there already.\n- **`[output.html.redirect]`**: each key is a path from the site root, and its value is a URL relative to the key's folder (`\"../infra/x.html\"`), root-relative, or external. Resolve the value against the key's folder, then map it like a chapter (`.html` → route, `index.html` → its folder); external URLs stay. Check each target against the old build: one that already led to a 404 → the page that replaced it, or drop it, and report.\n- **Fragment redirects** (`\"/page.html#old\" = \"other.html#new\"`, 0.4.52 and later): servers never see fragments. A heading renamed on the same page → pin the old id on it (`[#old]`); moved to another page → no server equivalent (report). A deleted page's own entry is its redirect.\n- **No equivalent, no redirect:** `print.html` (the whole book on one page, `noindex`), `toc.html` (the sidebar frame, `noindex`), and `404.html` (Blume writes its own). Mention `export: { pdf: true }` for printing.\n- **Hosts:** a static build writes each redirect as a meta-refresh page at `dist/<from>/index.html`, plus `_redirects` and `vercel.json`. On Netlify use `netlify({ output: \"static\" })` for real 301s (SKILL.md). GitHub Pages reads neither file and serves the meta-refresh page; how it answers `/x.html` there is unverified, so check a few old URLs after deploying.\n- **Heading anchors:** mdBook 0.5 ids match Blume's for most headings; 0.4 lowercases ASCII only (`## Über` → `#Über`), and both count the H1, so `# Install` then `## Install` gives `#install-1`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old old-book` (add `--map dist/blume-redirects.json` when routes changed, built with no host adapter; `--write` to apply), and rebuild until it reports 0.\n- **Check every old URL** after `blume build`. With no host adapter (static), from the Blume project:\n\n  ```bash\n  node -e '\n  const fs = require(\"node:fs\");\n  const moved = new Map(JSON.parse(fs.readFileSync(\"dist/blume-redirects.json\", \"utf8\")).map((r) => [r.from, r.to]));\n  const built = (to) => /^https?:/.test(to) || fs.existsSync(`dist${to.replace(/#.*/, \"\").replace(/\\/$/, \"\")}/index.html`);\n  const misses = fs.readFileSync(\"old-urls.txt\", \"utf8\").split(\"\\n\").filter(Boolean).filter((url) => {\n    const to = url.endsWith(\"/index.html\") ? url.slice(0, -\"index.html\".length) : moved.get(url);\n    return !(to && built(to));\n  });\n  console.log(misses.length ? misses.join(\"\\n\") : \"every old URL resolves\");'\n  ```\n\n  Then run `blume audit --only redirects`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts`: `blume` plus the `dev`/`build`/`preview`/`validate` scripts, with the excerpt step first. Commit the lockfile (SKILL.md step 6).\n- **`.gitignore`:** replace `book/` with `dist/`, `.blume/`, `node_modules/`, `.env.local`, and `<content root>/_excerpts/`.\n- **CI:** replace `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, and the plugin installs with Node 22, an install, `npm run build`, and `npm run validate`. A deploy workflow in another repo needs the same change: list it as a follow-up. For GitHub Pages, follow https://useblume.dev/guides/markdown-docs-github-pages (Actions as the Pages source), with `npm run validate` and `npm run build` in place of its `blume` steps so the excerpts exist, run from the Blume project's folder, and that folder's `dist` uploaded. A deploy that pushes a branch must publish `.nojekyll`, or Jekyll drops Blume's `_astro/` folder; `peaceiris/actions-gh-pages` adds it by default.\n- **`cargo doc` output published beside the book** (`/api/…`): keep building it in CI and copy `target/doc` into the build output, or link to docs.rs. Blume doesn't generate it.\n- **Several builds of the book** (a stable tag and `main`, `MDBOOK_BOOK__LANGUAGE` per language): migrate the current one; older versions → `versions`, or stay on the old host (report). Translations in `po/*.po` (mdbook-i18n-helpers) apply at build time: render each language's Markdown with `MDBOOK_BOOK__LANGUAGE=<xx>` and the Markdown renderer into `<xx>/`, then set `i18n`. Report it.\n- **Prettier** on the book: extend its `*.md` overrides to `*.mdx`, and keep `{/* */}` comments on one line. Prettier 3 skips what `.gitignore` ignores, so a `.gitignore` that covers the content root also makes a `prettier --check` CI step check nothing.\n- **A hand-written `llms.txt` in `src/`:** Blume generates `/llms.txt`, so delete the file. Its intro goes to `agents.llmsTxt.details`. A per-page summary becomes that page's `description` only where it reads well as the page's lede, since Blume shows `description` under the title; report the rest, and the sections that list things other than pages.\n- **Delete** `book.toml`, `SUMMARY.md`, `theme/`, the `additional-css`/`additional-js` files once ported, `mdbook-admonish.css`, `mermaid.min.js`, `mermaid-init.js`, and the `book/` folder. Grep the repo for `mdbook`, `book/`, and the old `.html` URLs (READMEs, CONTRIBUTING, examples, bots).\n\n## Dropped — report these\n\nSection numbers, separators, and draft chapters; suffix chapters and leaf chapters that moved above folders; the print page; the theme switcher and themes beyond one light and one dark; search options; the play button, the editor, and runnable or editable examples; hidden lines and their toggle; `mdbook test` coverage of hand-written blocks; `rustdoc_include` context; admonish title bars, colors, icons, custom directives, and anchors; fragment redirects across pages; the old site's empty blocks from missing anchors, and what you did with each; definition-list styling; Font Awesome icons with no Lucide match; `class=\"hidden\"` content; plugin output you froze (cmdrun, templates, custom preprocessors); a hand-written `llms.txt`; whole-book PDF or EPUB output; and every plugin you removed without an equivalent.\n",".agents/skills/blume-migrate/references/mintlify.md":"# Mintlify → Blume\n\nThe deepest migration. Mintlify declares its **entire navigation in `docs.json`**; Blume derives navigation from the filesystem. The bulk of the work is reshaping content into folders + tabs and translating config, callouts, and icons.\n\n## Detect\n\n- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.\n- A `mintlify` dependency / `mintlify dev` script.\n- Content is `.mdx` under the repo root (Mintlify has no `content.root` — pages live at the top level), with a `/snippets` folder and image dirs like `/images`.\n\nRead `docs.json` first; it drives everything.\n\n## Config: `docs.json`/`mint.json` → `blume.config.ts`\n\nResolve `$ref` includes first (Mintlify splits config across files). Map only what's set:\n\n| Mintlify | Blume | Notes |\n| --- | --- | --- |\n| `name` / `title` | `title` |  |\n| `description` | `description` |  |\n| `logo` (string or `{ light, dark, href }`) | `logo` (string, or `{ image: { light, dark, alt }, text, href }`) | Mintlify's `light`/`dark` nest under Blume's `image`; ensure files land in `public/`. **If the SVG is local and monochrome** (a solid black or white mark), rewrite its `fill`/`stroke` to `currentColor` and collapse `{ light, dark }` to the string shorthand `logo: \"/logo.svg\"` — one file that inherits the theme text color and flips with light/dark automatically. If the logo is a **wordmark** (brand name baked in), set `text: \"\"` so it doesn't render twice beside `title` |\n| `favicon` (string **or `{ light, dark }`**) | **drop the field** — copy the file(s) into `public/` under conventional names (`favicon.svg`/`icon.png`) | Blume auto-detects by filename; there is **no** favicon config field. A `{ light, dark }` source maps to a filename pair: light file → `public/icon.png`, dark file → its `-dark` sibling `public/icon-dark.png` (same directory and extension — convert if the formats differ) |\n| `colors.primary` + `colors.light` | `theme.accent` (`{ light, dark }`) | accent is now **per-mode**: `accent: { light: <colors.primary>, dark: <colors.light> }`. If Mintlify sets only `primary`, collapse to the string shorthand `accent: \"<colors.primary>\"` (applies to both modes) |\n| `colors.dark` | `theme.action` |  |\n| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) |  |\n| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |\n| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |\n| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |\n| `background.decoration` | **drop** | no Blume equivalent |\n| `fonts.family` / `fonts.{heading,body}.family` | `theme.fonts.{display,body}` | a curated slug (kebab-case, e.g. `space-grotesk`) maps directly; any other Google family maps to the object form `{ name: \"Family Name\" }`; a self-hosted font (`fonts.*.src` URLs) maps to `{ name, variants: [{ src, weight }] }` after downloading the files into the project |\n| `banner` | `banner` (`{ content, dismissible, id, link: { href, text } }`) | **only** those keys — drop `banner.color`/`banner.type` |\n| `styling.latex: true` | **drop the field** — block math `$$…$$` renders in `.mdx` with no config | there is **no** `markdown.math` field; Blume writes inline math with two dollar signs too — convert each inline `$…$` to `$$…$$` inside the sentence, checking every pair is math and not currency or a shell variable |\n| `styling.codeblocks.theme` | `markdown.code.theme` (`{ light, dark }`) |  |\n| `search.prompt` | **drop** | no equivalent |\n| `seo.metatags` | `seo.metatags`, minus the tags Blume writes itself | keep verification tokens, `theme-color`, and the like as written; Blume refuses `description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, and `article:*` dates, so move those to their settings (`og:image` → `seo.og` or a page's `seo.image`, `twitter:site` → `seo.x.handle`) and drop the rest. `seo.paths` (folder tags) has no equivalent: set per-page `seo` frontmatter |\n| `seo.indexing: \"all\"` | `search.indexing.includeHiddenPages: true` |  |\n| `variables` (`{{name}}`) | `variables` | same syntax and names (letters, digits, `_`, `-`); pages keep their `{{name}}` references unchanged |\n| `integrations.posthog` (`{ apiKey, apiHost, sessionRecording }`) | `posthog({ key, host })` in the `analytics` list | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`); `sessionRecording: false` → `disable_session_recording: true`; every adapter is imported from `blume/analytics` |\n| `integrations.ga4` (`{ measurementId }`) | `googleAnalytics({ id })` |  |\n| `integrations.gtm` (`{ tagId }`) | `googleTagManager({ id })` |  |\n| `integrations.plausible` (`{ domain, server }`) | `plausible({ domain, host })` | `server` is a bare hostname; `host` is an origin — prepend `https://` |\n| `integrations.fathom` (`{ siteId }`) | `fathom({ site })` |  |\n| `integrations.pirsch` (`{ id }`) | `pirsch({ code })` |  |\n| `integrations.mixpanel` (`{ projectToken, region }`) | `mixpanel({ token, region })` |  |\n| `integrations.amplitude` (`{ apiKey }`) | `amplitude({ key })` |  |\n| `integrations.segment` (`{ key, cdnUrl }`) | `segment({ key, cdn })` |  |\n| `integrations.hightouch` (`{ writeKey, apiHost }`) | `hightouch({ key, host })` |  |\n| `integrations.heap` (`{ appId }`) | `heap({ id })` |  |\n| `integrations.hotjar` (`{ hjid, hjsv }`) | `hotjar({ id, version })` | both numbers |\n| `integrations.clarity` (`{ projectId }`) | `clarity({ id })` |  |\n| `integrations.logrocket` (`{ apiKey }`) | `logrocket({ id })` |  |\n| `integrations.clearbit` (`{ publicApiKey }`) | `clearbit({ key })` |  |\n| `integrations.adobe` (`{ launchUrl }`) | `adobe({ url })` |  |\n| any other `integrations` script | `script({ src, strategy, attributes })` / `vercel()` in the `analytics` list | one `script()` adapter per provider without a factory |\n| `telemetry` (`{ enabled: false }`) | `feedback: false` | Mintlify's switch also turned off its feedback widgets; Blume sends no telemetry of its own, so only the rating needs turning off |\n| Feedback toggles in the Mintlify dashboard (not in `docs.json`) | `feedback: { comments: true }` for contextual (written) feedback | ask whether the site collected written feedback; Blume sends comments through the `analytics` adapters as `feedback_comment` events. Code-snippet feedback, edit suggestions, and raise-issue have no equivalent: report them |\n| `integrations.cookies` (`{ key, value }`), or a content-folder `.js` file that injects Osano or Ethyca/Fides | `consent`, imported from `blume/consent`: `osano({ customerId, configId })` (both IDs from the `cmp.osano.com/<customerId>/<configId>/osano.js` URL), `ethyca({ privacyCenter })` (the origin that serves `fides.js`), or `native()` for Blume's own banner | Mintlify's `cookies` pair only switched off its own telemetry for readers whose localStorage lacked the value; under `consent`, every `analytics` adapter waits for the reader instead. Delete the injector script. Another consent manager (Transcend, OneTrust, …) has no adapter: report it |\n| A root `skill.md`, or `.mintlify/skills/<name>/SKILL.md` | move each into a `skills/<name>/` folder and set `agents.skills: \"./skills\"`; drop Mintlify-only frontmatter (`groups`) | Blume already generates a skill for the site at `/skill.md` (`agents.skillMd`, on when `deployment.site` is set); a root `skill.md` replaces it only if its `name` matches the site's title slug (`Acme Docs` → `acme-docs`), so set that name |\n| `contextual` (`[\"copy\",\"chatgpt\",\"claude\",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `agents.mcp.enabled` + server output (report as a follow-up) |\n| `footer` (`{ socials, links }`) | `footer: { socials, links }` | `socials` keeps its keys, except `twitter`/`x-twitter` → `x` and `earth-americas` → `website`; a key outside Blume's list drops (report); `links` is one row of `{ label, href }`: every column's `items` flatten into it in order, and each column's `header` drops (report) |\n| `api.mdx` (`{ server, auth: { method, name } }`) | `api: { server, auth: { method, name } }` | the defaults for `api`-frontmatter pages |\n| `api.playground` (`{ display, proxy }`) | `api: { playground }` | `display: \"none\"` → `playground: false`; `\"simple\"` has no site-wide form, so set `playground: simple` in each page's frontmatter; `proxy` (on by default in Mintlify) → `playground: { proxy: true }`, which needs a server-output adapter, or leave it off to send directly |\n| `redirects` (`{ source, destination, permanent }`) | `redirects: [{ from, to, status }]` | `source` → `from`, `destination` → `to` as written, patterns included (`/beta/:slug*`, `/old/article-*`); `permanent: false` → `status: 307`, otherwise `status: 308` |\n| `navigation.languages` | `i18n` | see i18n below |\n\nA minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.\n\n## Icons: FontAwesome → Lucide (required)\n\nMintlify defaults to **FontAwesome**; Blume is **Lucide-only**. Convert every icon reference — frontmatter `icon`, nav-group `icon`, `<Icon>`, `<Card icon>` — to the closest Lucide name. Discard Mintlify's `iconType` (solid/regular/brands) entirely.\n\n**Automate the frontmatter pass — don't hand-edit it.** `scripts/mintlify-codemod.mjs` (in this skill, zero-dependency) remaps every **frontmatter** `icon:` using the table below, drops icons with no Lucide equivalent, and — in the same pass — drops/renames unsupported frontmatter keys (see [Frontmatter](#frontmatter)). It's deterministic and idempotent, and reports what it changed per file plus what it couldn't (unknown icons, OpenAPI-stub flags):\n\n```bash\n# <skill> = this skill's directory (the one containing SKILL.md); dry run first (report only), then apply:\nnode <skill>/scripts/mintlify-codemod.mjs <content-dir>\nnode <skill>/scripts/mintlify-codemod.mjs --write <content-dir>\n```\n\nThe codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon=\"…\">`, `<Card icon=\"…\">`, nav-group icons in `docs.json`) it does not see — convert those by hand using the same table. Common mappings:\n\n| FontAwesome | Lucide |  | FontAwesome | Lucide |\n| --- | --- | --- | --- | --- |\n| `bolt` | `zap` |  | `gear`/`cog` | `settings` |\n| `circle-info`/`info` | `info` |  | `wand-magic-sparkles`/`magic` | `sparkles` |\n| `house` | `house` |  | `magnifying-glass` | `search` |\n| `gauge`/`gauge-high` | `gauge` |  | `puzzle-piece` | `puzzle` |\n| `envelope` | `mail` |  | `file-lines` | `file-text` |\n| `pen-to-square` | `square-pen` |  | `trash-can` | `trash-2` |\n| `xmark`/`times` | `x` |  | `circle-check` | `circle-check` |\n| `triangle-exclamation` | `triangle-alert` |  | `circle-exclamation` | `circle-alert` |\n| `arrow-right-from-bracket` | `log-out` |  | `right-to-bracket` | `log-in` |\n| `location-dot`/`map-marker` | `map-pin` |  | `comments` | `messages-square` |\n| `cube` | `box` |  | `cubes`/`boxes` | `boxes` |\n| `layer-group` | `layers` |  | `diagram-project`/`sitemap` | `workflow`/`network` |\n| `chart-line` | `chart-line` |  | `chart-simple`/`chart-column` | `chart-column` |\n| `flask` | `flask-conical` |  | `robot` | `bot` |\n| `screwdriver-wrench`/`toolbox` | `wrench` |  | `circle-question`/`question` | `circle-help` |\n| `life-ring` | `life-buoy` |  | `shield-halved` | `shield` |\n| `rocket`/`book`/`book-open`/`code`/`terminal`/`key`/`lock`/`user`/`users`/`database`/`server`/`cloud`/`bell`/`calendar`/`star`/`heart`/`tag`/`folder`/`globe`/`link`/`download`/`upload`/`check`/`copy`/`play`/`filter` | _(same name — verify)_ |\n\n**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*`): Lucide still ships a few brand marks, and Blume renders them under the same name — `facebook`, `github`, `gitlab`, `instagram`, `linkedin`, `slack`, `twitter`, `youtube`. Keep those. The rest (`discord`, `x-twitter`, `docker`, `npm`, `python`, `react`…) have **no** Lucide equivalent, and FontAwesome's `apple` is the Apple logo while Lucide's `apple` is the fruit. Where no Lucide counterpart exists, **drop the icon and report it**. An icon that only marks a link to the repository or a social profile usually has a better home: the `github` config (renders the footer's repo link) or `footer.socials`. The build won't catch a wrong name for you: an unknown icon renders nothing, and it's only a warning (`BLUME_UNKNOWN_ICON`) for navigation icons — an icon prop on a component like `<Card>` fails silently — so check every name.\n\n## Navigation: `docs.json` `navigation` → filesystem + tabs\n\nMintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:\n\n- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. `directory` (`card`/`accordion`/`none`, on a group, tab, or the navigation itself) → `meta.ts` `directory` on that folder, or on the content root's `meta.ts` for the whole navigation; it inherits down the tree the same way, and lists the pages on the group's `root`/`index` page. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display, directory })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)\n- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.\n- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.\n- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `\"GET /path\"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).\n- **`tabs`** (top-level `navigation.tabs`, e.g. `App` / `API` / `API reference`) → Blume's **header tabs** `navigation.tabs` (`{ label, path, icon? }`). **Keep them as tabs — do not flatten them into `navigation.sidebar` groups or sections.** Flattening is the easy translation error: it collapses the site's top-level structure into one global sidebar and loses the header tab bar entirely. A Mintlify top-level tab is a header tab; map it to one. Put each tab's pages in **one folder** and point the tab's `path` at it — the tab then scopes the sidebar automatically (under the tab's `path`, the sidebar shows only that folder). **Blume resolves the active tab by URL prefix** (the tab whose `path` is the longest prefix of the current route), so every page in a tab must live under that tab's single `path`. Mintlify tabs freely mix pages from any folder / arbitrary routes; Blume tabs scope by prefix, so a tab that pulls in routes from all over is **not portable as-is**. You have two choices, per tab:\n  - **Move the pages under one prefix** so they share the tab's `path` (a route change → add a `redirects` entry for each moved page). This preserves the exact tab grouping.\n  - **Accept the closest shape** — assign each shared/cross-cutting page to one tab's folder and link to it from the others — when moving routes isn't worth the churn.\n\n  Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.\n\n- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.\n- **`anchors` / `navigation.global.anchors`** (persistent sidebar-top links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old \"drop and report\" for anchors.)\n- **`navbar.links`** (plain header links — Log in, Status, Support) → **`navigation.actions`** (`[{ label, href }]`), rendered in the header left of the icon buttons. A GitHub link → the `github` config (or `navigation.repo` as a URL when the docs repo is private) instead; it renders in the footer, the only place Blume links a repository. Header links hide on phones; a link that must survive there belongs in `featured`.\n- **`navbar.primary`** (the header **CTA button** — \"Get Started\"/\"Sign Up\", `type: \"button\"`) → **`navigation.cta`** (`{ label, href }`, singular: Blume renders exactly one filled button). A `type: \"github\"` primary → the `github` config instead. A route the docs don't serve (the product's own `/signup`) must be an absolute URL, or it raises `BLUME_NAV_MISSING_PAGE`.\n- **`languages`** → `i18n`, not a selector (see below).\n- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.\n\nThis reshaping **changes URLs** — a page moved from `getting-started/quickstart` into the API tab's folder becomes `/api/…`, an `index` promotion drops a segment, etc. Record each old→new path and add a `redirects` entry for it (see below); otherwise every existing link and bookmark 404s.\n\n## Content & component transforms\n\nRewrite each page's MDX:\n\n- **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type=\"x\">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. A callout with an `icon` or `color` (Mintlify's generic `<Callout icon=\"key\" color=\"#FFC107\">`) stays a `<Callout>` instead, since a directive can't set them: Blume's takes `type`, `title`, `icon` (remap it with the table below), and `color` (any CSS color); drop `iconType`.\n- **Accordions — container/item inversion!** Mintlify nests `<Accordion title=\"…\">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title=\"…\">`→`<AccordionItem title=\"…\">` (item).\n- **`<Tabs>` → `<Tabs inline>`.** Mintlify renders tabs **borderless** — a strip on a full-width rule with the content flowing beneath as prose — while Blume's `<Tabs>` defaults to a bordered box. Add `inline` to each `<Tabs>` to preserve Mintlify's appearance; every child `<Tab title=\"…\">` is unchanged. Don't add `param` — Mintlify tabs switch in place and don't deep-link to the URL, so plain `inline` is the faithful mapping.\n- **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tab>`, `<Steps>`/`<Step>`, `<View>` (its `title` becomes an option in the page's view picker, as on Mintlify; remap its `icon` with the table below and drop `iconType`). Keep them as-is.\n- **Code-block titles carry over.** Both read every bare word after the language as the title (` ```ts Acme SDK `), and `wrap` and `expandable` mean the same in both. Mintlify's other fence options don't: `lines` → `lineNumbers` (left as is, it does nothing and warns `BLUME_CODE_FENCE_OPTION`), `highlight={1-2,5}` → `{1-2,5}`, `focus={2}` → a `// [!code focus]` comment on each line, and drop `icon=\"…\"`.\n- **`<RequestExample>`/`<ResponseExample>` pass through.** Top-level ones pin to a column beside the page on wide screens, request above response, as on Mintlify; `dropdown` works the same.\n- **Hand-written endpoint pages pass through.** `api: \"POST /v1/users\"` frontmatter, with `authMethod` (`bearer`/`basic`/`key`/`none`) and `playground` (`interactive`/`simple`/`none`), works as on Mintlify: the page's `<ParamField>`s build its Try it panel and request samples. Keep them as written.\n- **API fields pass through.** `<ParamField>` (`path`/`query`/`header`/`body`, `type`, `required`, `deprecated`, `default`, `placeholder`) and `<ResponseField>` (`name`, `type`, `required`, `deprecated`, `default`, `pre`, `post`) render natively, nested `<Expandable>` fields included. Keep them as written. An older `<RequestField>` isn't supported: rewrite it as a `<ParamField>`.\n- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `githubReleases()` source.\n- **Snippets become includes.** Blume has no `/snippets` import mechanism; its reuse is `<include>`. Move each snippet into the content folder as a `_`-prefixed partial (`_snippets/x.mdx`, which never renders as a page), and replace `import X from \"/snippets/x.mdx\"` + `<X prop=\"v\" />` with `<include prop=\"v\">./_snippets/x.mdx</include>` on its own line. In the partial, rewrite each `{prop}` placeholder to `{{prop}}`: include attributes are props read with the variables syntax. Named string imports (`import { foo } from \"/snippets/vars.mdx\"`) → a site-wide `variables` entry read as `{{foo}}`, or inline the value.\n\n## Frontmatter\n\nMintlify page frontmatter → Blume's strict schema. **`scripts/mintlify-codemod.mjs --write` does the mechanical rows automatically** (rename, drop, icon-remap) and flags the judgment rows (`openapi`/`asyncapi`) for you — see [Icons](#icons-fontawesome--lucide-required) for the invocation. The table is both the mapping reference and a description of exactly what the codemod does:\n\n| Mintlify | Blume | Codemod |\n| --- | --- | --- |\n| `title` / `description` | pass through | — |\n| `sidebarTitle` | `sidebar.label` | renames |\n| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |\n| `tag` | `sidebar.badge` | renames |\n| `canonical` | `seo.canonical` | renames |\n| `og:image` | `seo.image` | renames |\n| `hidden: true` | valid top-level in Blume — **kept as-is**; add `noindex: true` yourself if the page must also leave the search index | left (do by hand) |\n| `openapi`/`asyncapi` | usually an endpoint stub → **delete the page** (Blume generates operation pages); else drop the key and keep it as a normal page | **flags** for review — never auto-deletes a page |\n| `api`, `authMethod`, `playground` | kept as written (a hand-written endpoint page, see above) | left |\n| `related` (a list) | kept as written: root-relative paths, URLs, and `{ Title: link }` entries render as Related pages cards | left; `related: true` has no equivalent (Blume has no automatic related pages), so drop it (report) |\n| `mode` | `mode`, as written | `default`, `wide`, `center`, `custom`, and `frame` carry over; `assistant` has no equivalent → drop (report) |\n| `hideFooterPagination: true` | `pagination: false` | renames (`false` drops, since the links show by default) |\n| `keywords`, `boost` | `search.keywords`, `search.boost` | renames |\n| `searchable: false` | `search.exclude: true` | renames (`searchable: true` drops, since pages are searchable by default) |\n| `public`, `rss`, `groups`, `hideApiMarker`, `iconType` | **drop** (report) | drops |\n\nThe codemod leaves the source key in place and reports a conflict rather than clobbering data when a rename target already exists (e.g. a page already has `sidebar.label`) or the value is too structured to move safely — resolve those by hand. Remove any duplicate H1 in the body — `title` renders the H1. (The codemod only edits frontmatter; it never touches the body.)\n\n## OpenAPI\n\nTop-level `openapi`, `api.openapi`, or a per-group/per-tab `openapi` → `reference: [openapi({ sources: [{ spec, label?, route? }] })]`, with `openapi` imported from `blume/reference` (`spec` alone is the single-source shorthand; several per-tab specs become several sources, or several `openapi()` entries when they need different routes; a spec the source embedded with Scalar becomes a `scalar({ spec })` entry). A Mintlify `{ source, directory }` object: `directory` → the source's `route`. An `asyncapi` field maps the same way to `asyncapi({ … })` in the list. **Delete every per-endpoint stub page** (frontmatter `openapi: \"GET /path\"`, `openapi: \"spec.json webhook name\"`, or a `\"GET /path\"` nav entry) — Blume's native renderer generates one real page per operation and per webhook, and shows callbacks on their operation's page. **Add a `navigation.tabs` entry pointing at the reference `route` yourself** (Mintlify's API tab maps to it); the reference does not create a header tab automatically.\n\n- **Vendor the spec.** Mintlify usually points at a spec **URL**. Copying that straight into `spec:` makes every build fetch it at build time — a single point of failure in CI/offline/behind a proxy, and a failed fetch silently drops the reference (leaving the tab pointing at a route that 404s). Prefer downloading it into the repo (`curl … -o openapi/<name>.json`) and pointing `spec` at that local path. If you keep the URL, report the dependency and consider a `prebuild` refresh-with-fallback.\n- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/list-models`: a camelCase tag or id is split into kebab-case, not just lowercased — see SKILL.md \"OpenAPI\") — this differs from Mintlify's endpoint URLs, so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route and flags the ones you miss.\n- **Overlays carry over.** An `openapi: { source, overlays: [...] }` object → that source's `overlays` (`openapi({ spec, overlays })`, or `overlays` on each entry in `sources`), in the same order; Blume applies Overlay 1.0 and 1.1 the same way. An overlay Mintlify found only through its `extends` field (auto-discovered, not listed in `docs.json`) must be listed explicitly: Blume doesn't scan for overlays.\n- **Code samples carry over.** `api.examples.languages` → `openapi({ codeSamples: [...] })`; Blume generates the same 18 languages and accepts Mintlify's keys and aliases (`bash`, `javascript`, `node`, `csharp`, `dotnet`, `c++`, …) as written. `api.examples.autogenerate: false` → `codeSamples: false`. `x-codeSamples` in the spec needs nothing: Blume renders them ahead of the generated samples.\n- **Keep the \"Introduction\" page.** Mintlify commonly has a written intro/auth page in an \"Introduction\" group beside the \"Endpoints\" (openapi) group in the same tab. Keep it: a normal content page placed under the `openapi()` adapter's `route` (e.g. `<root>/api-reference/introduction.mdx`) merges into the reference tab's sidebar alongside the generated operations. Delete only the per-endpoint stubs, not the conceptual pages.\n\n## Assets\n\nMintlify serves every top-level dir (e.g. `/images`) at the site root. Blume serves `public/` at the site root. **Move root asset dirs into `public/`** (`mv images public/images`) — every `/images/...` reference still resolves, unchanged. Move loose root files (`logo.png`, `favicon.png`) under `public/` too.\n\n## i18n\n\n`navigation.languages` (≥2) → `i18n: { defaultLocale, locales: [{ code, label }] }`. The `default: true` language → `defaultLocale`. Translated content already lives in ISO-code directories, which match Blume's `dir` parser — no file moves. Remove any language selector; language switching is handled by i18n.\n\nPer-language `banner`, `navbar`, and `footer` (on `navigation.languages[]`) merge into the one site-wide config, with each label as a map of language code to that language's text: `banner: { content: { en: \"…\", fr: \"…\" } }` (and `link.text`), `navigation.actions`/`cta` labels, and `footer` link labels. Hrefs stay single — Blume moves an internal link into the reader's language itself. A language without an entry shows the default language's text. Report what doesn't merge: a banner or link set for only some languages, or one whose href differs per language.\n\n## Dropped — report these\n\n- **A group's `boost`** (on a `navigation` group, inherited by its pages) → no inherited boost; set `search.boost` on the pages that need it.\n- **Authentication** (password or SSO, set in the Mintlify dashboard) → Blume has none of its own; point the user at host-level protection (Vercel Deployment Protection, Netlify password protection, Cloudflare Access), per the deployment docs' **Private docs** section. It covers the whole site, so a mix of public and private pages needs two sites.\n- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`.\n",".agents/skills/blume-migrate/references/mkdocs.md":"# MkDocs (Material for MkDocs, Zensical) → Blume\n\nMkDocs is a Python generator: one YAML config (`mkdocs.yml`), Markdown in `docs/`, and Python-Markdown extensions for everything beyond CommonMark. Nearly every MkDocs site uses the **Material for MkDocs** theme, so pages are full of extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets, `{ #id }` attribute lists), and navigation is an explicit `nav` list in config (or alphabetical). The work is translating `mkdocs.yml`, rebuilding `nav` as folders and `meta.ts`, and converting the syntax — most of which becomes `.mdx`-only Blume features, and **MDX rejects several things Python-Markdown accepts**.\n\nScope: checked against MkDocs 1.6.1, Material for MkDocs 9.7.6–9.7.7, pymdown-extensions 11.0.1–12.1, mkdocs-redirects 1.2.3, awesome-pages 2.10.1, awesome-nav 3.3.0, literate-nav 0.6.3, mkdocs-static-i18n 1.3.1, mkdocs-macros 1.5.0, mike 2.2.0, an end-to-end migration of astral-sh/uv (84 pages), and the codemod on pdm-project/pdm (23 pages). **Zensical** and **ProperDocs** projects are the same migration. MkDocs 2.0 (a pre-release with a TOML config and no plugins) was not checked.\n\n## Run the codemod first\n\nFrom the folder that holds `mkdocs.yml` (keep the old Python setup until the end, so `mkdocs build` can still produce the old site for comparison):\n\n```bash\nnode <skill>/scripts/mkdocs-codemod.mjs docs           # dry run: report only\nnode <skill>/scripts/mkdocs-codemod.mjs --write docs   # apply\n```\n\nIt reads `mkdocs.yml` (or `zensical.toml`/`properdocs.yml`, `INHERIT` included) for the enabled extensions, the snippets `base_path`, `extend_pygments_lang`, and `nav` titles, then per page: converts admonitions, details, and tabs (indentation-aware, nested); turns snippets into `<include>` (copying ones from outside the content root into `_snippets/`); rewrites fences, `#!lang` inline code, heading ids, attribute lists, keys, `==mark==`/`^^ins^^`, icon shortcodes, `\\(x\\)` math, and abbreviation lines; moves the body H1 (ATX or setext) into `title` (inline Markdown stripped — Blume renders backticks in `title` literally), a differing `nav` title into `sidebar.label`, and a differing frontmatter `title` into `seo.title`; maps `hide`/`status`/`tags`/`icon`; renames to `.mdx` **only** the pages that now need MDX and fixes their comments, autolinks, and void tags; renames `README.md` to `index.md`; and rewrites links and `<include>`s to every renamed file. It is idempotent.\n\nIt **reports**, without changing, what needs judgment: mkdocstrings blocks, macros/Jinja, template overrides, hooks, every plugin (with a hint), mike and `extra.alternate`, snippet line ranges, `///` blocks, grid cards, code annotations, unknown icons, raw `<img>`s with a relative `src`, pages left with no title, snippet files that publish as pages too, `nav` entries with no file, and every MDX hazard left in a renamed page. Work through that report with the sections below. Nav, config, redirects, and repo integration stay yours.\n\nAfter the first `blume build`, pin the old heading ids (MkDocs and Blume slug headings differ — see URLs). Point `--old` at the old site's built HTML (`mkdocs build -d <dir>` before you delete `mkdocs.yml`) or its live URL, and run it from the Blume project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url>           # dry run\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url> --write\n```\n\nRebuild and rerun until it reports 0 (add `--map dist/blume-redirects.json` when pages moved).\n\n## Detect\n\n- **`mkdocs.yml` / `mkdocs.yaml`** — Material when `theme.name: material`; the plain `mkdocs`/`readthedocs` themes migrate the same way with fewer extensions. Sometimes in a subfolder (`docs/mkdocs.yml`), which `blume migrate` doesn't detect from the repo root: name the source, and run the codemod from that folder.\n- **`zensical.toml`** — Zensical's format: MkDocs' keys in TOML, usually under `[project]` (`[project.theme]`, `nav = [ { \"Title\" = \"page.md\" } ]`); extensions as `[project.markdown_extensions]` keys (`admonition = {}`, `pymdownx.tabbed.alternate_style = true`) or `[project.markdown_extensions.<name>]` tables. With **no extensions configured**, Zensical enables a default set (admonition, details, tabbed, attr_list, md_in_html, keys, mark, caret, arithmatex, emoji, and more, but not snippets); the codemod applies it to a `zensical.toml`. Zensical also builds a plain `mkdocs.yml` and applies the same defaults when it sets no `markdown_extensions`: the codemod can't tell, so list the extensions the pages use in a copy of the config and pass it with `--config`.\n- **`properdocs.yml` / `.yaml`** — ProperDocs, a continuation of MkDocs 1.x.\n- Python deps, not `package.json`: `mkdocs`, `mkdocs-material`, `zensical`, `properdocs` in `requirements*.txt` or a `pyproject.toml` docs group; `mkdocs build`/`gh-deploy` in CI; `.readthedocs.yaml` with an `mkdocs:` key.\n\nResolve the config first: `INHERIT: base.yml` deep-merges this file over another (a repo may have several configs on one base — migrate the public one, report the rest). `!ENV [VAR, default]` reads an environment variable: use the default and report it. `!relative` and `!!python/name:` tags break JS YAML parsers — read the file as text.\n\n**Where the Blume project goes.** `blume build` empties `<project>/dist`, and a Python or Rust repo root often uses `dist/` for wheels or builds. When `mkdocs.yml` sits at the root of a code repo, put `blume.config.ts` and `package.json` **in the docs folder** with `content.root: \".\"` and `content.include` scoped to the page folders (`[\"index.mdx\", \"{guides,concepts,reference}/**/*.{md,mdx}\"]`), and say so. That also keeps a `package.json` out of a non-JS repo root.\n\n**Generated pages.** A `nav` entry or link whose file isn't in the repo is generated at build time (`gen-files`, a hook, a CI step, a `.gitignore`d output — the codemod lists them). Convert today's output once so the site builds, then report that the **generator must change**: until it writes Blume syntax, CI regenerates the old one and Blume renders it as text with a green build. If conversion renames the output (`settings.md` → `settings.mdx`), update `.gitignore`, `.prettierignore`, and the generator's own check mode too.\n\n## Config: `mkdocs.yml` → `blume.config.ts`\n\nMap only what's set:\n\n| MkDocs | Blume |\n| --- | --- |\n| `site_name` / `site_description` | `title` / `description` |\n| `site_url` | Its origin → `deployment.site` (unless the target is Vercel or Netlify — see SKILL.md); **a path in it** (`https://docs.acme.com/uv/`) → `deployment.base: \"/uv\"`, on every host. mike adds its version segment after `site_url`, so it isn't part of the base (Versioning) |\n| `docs_dir` | `content.root` (`docs` is the default) |\n| `use_directory_urls` | no setting — see URLs |\n| `repo_url` on GitHub | `github: { owner, repo }` |\n| `edit_uri` / `edit_uri_template` | **`github.branch`** from the segment after `edit/` or `blob/`. Unset with a GitHub `repo_url` means `edit/master/docs/` → `branch: \"master\"` (Blume defaults to `main`). The path after the branch is `docs_dir`, which Blume adds — **not** `github.dir` (that's only for a project in a repo subfolder). `edit_uri: \"\"`, or Material without the `content.action.edit` feature, shows no edit button; Blume has no separate switch, so accept edit links or leave `github` unset and set `navigation.repo: \"<repo_url>\"` (a footer mark) — say which. Leave `github` unset if edit links would point at gitignored generated files |\n| `repo_url` elsewhere (GitLab…) | no edit links; a header link → `navigation.actions` (report) |\n| `repo_name`, `site_author`, `site_dir`, `dev_addr`, `remote_*`, `watch`, `strict`, `validation` | drop |\n| `copyright` | `footer.copyright`, plain text: strip any HTML tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`) |\n| `exclude_docs` | `content.exclude` — the list adds to the default `\"**/_*\"` and `\"**/.*\"`; `\"!**/_*\"` publishes `_` files |\n| `draft_docs` / `not_in_nav` | `draft: true` / `sidebar.hidden: true` on those pages |\n| `extra_css` | a project-root **`theme.css`**: Material's `--md-*` variables and `.md-*` classes don't exist — port only rules that still mean something, and read colors out of it (Theme) |\n| `extra_javascript` | delete MathJax/KaTeX/Mermaid loaders. Anything else → move into `public/` and `script({ src })` in `analytics` (from `blume/analytics`); a root-relative `src` (`\"/js/x.js\"`) loads under `deployment.base`. Code written against Material's `document$` or `.md-*` DOM won't run — report it |\n| `extra.social` | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube; `twitter`/`x-twitter` → `x`); the site's own repo is covered by `github`; other platforms drop (report) |\n| `extra.analytics` (`provider: google`) | `googleAnalytics({ id: property })`. A `custom_dir` partial `partials/integrations/analytics/<name>.html` is a custom provider: use its adapter (`fathom({ site })` from `data-site`, …) or `script()`. `feedback` → Blume's page feedback is on by default |\n| `extra.consent` | `consent: native()` (from `blume/consent`) |\n| `extra.version` (`provider: mike`) | Versioning |\n| other `extra` keys used as `{{ }}` | `variables` (Manual decisions: macros) |\n| `markdown_extensions` | nothing to configure. `toc.toc_depth: N` → `toc: { maxHeadingLevel: N }` (a `\"2-5\"` range: its upper bound) |\n| `plugins`, `hooks`, `theme.custom_dir` | Plugins; Manual decisions |\n\n### Theme\n\n| Material | Blume |\n| --- | --- |\n| `theme.palette` | one palette with `scheme: slate` → `theme.mode: \"dark\"`; with `default` → `\"light\"`; a list with toggles → system (the default) |\n| `palette.primary` (link color) | `theme.accent`. Presets `red`, `pink`, `purple`, `blue`, `green`, `orange`, `teal` map by name. Other names aren't usable CSS (`deep purple` warns `BLUME_THEME_COLOR_INVALID`, CSS `indigo` isn't Material's): use Material 9.7's link color (`--md-typeset-a-color`) as `{ light, dark }` — indigo `#4051b5`/`#5488e8`, deep purple `#7e56c2`/`#a47bea`, light blue `#02a6f2`, cyan `#00bdd6`, deep orange `#ff6e42`/`#ff764d`, brown `#795649`/`#c1775c`, amber `#d19d00`/`#ffc105`, lime `#8b990a`/`#cbdc38`, light green `#72ad2e`/`#8bc34b`, yellow `#b8a500`/`#ffec3d`; grey, blue grey, black, and white all link in `#4051b5`/`#5e8bde`. `custom` or a custom `scheme:` is defined in `extra_css`: take `--md-typeset-a-color` per scheme (and `--md-default-bg-color` → `theme.background` when it isn't plain white/black) |\n| `palette.accent` | drop |\n| `theme.font.text` / `.code` | `theme.fonts.body` (and `display`) / `mono` as `{ name: \"…\" }`; `font: false` → drop |\n| `theme.logo` (a file in `docs_dir`) | move into `public/`, `logo: \"/logo.svg\"` (SKILL.md on `currentColor` and wordmarks). `theme.icon.logo` → drop and report |\n| `theme.favicon` | move to `public/favicon.<ext>` — no config field |\n| `theme.icon.*`, `theme.language` (English) | drop. A non-English single-language site: the schema accepts a one-locale `i18n` block — check the UI language in `blume dev` |\n\n`theme.features`:\n\n| Feature | Blume |\n| --- | --- |\n| `navigation.tabs` | **`navigation.tabs`**, one per top-level `nav` section, each in one folder (SKILL.md \"Tabs\") |\n| `navigation.sections` | top-level sections stay Blume's default `flat` headers; set **`display: \"group\"` in the `meta.ts` of every second-level section**: Material renders those as collapsible, and a `flat` group lists its pages above its subgroups, reordering a section that interleaves them |\n| neither `sections` nor `tabs` | `navigation.sidebar.display: \"group\"` (Material's collapsible tree) |\n| `navigation.expand` | `collapsed: false` in those folders' `meta.ts` |\n| `navigation.indexes`, plugin `section-index` | a folder's `index` page links its group header. Blume also lists it as the group's first row, so the label shows twice. To show it once, as Material does, set `sidebar.hidden: true` on the index: that drops only the duplicate row, and the page stays in `sitemap.xml`, search, and `llms.txt`. A hidden index whose `title` differs from its folder's `meta.ts` `title` warns `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`: match the two titles, set the index's `sidebar.label` to the folder title to keep its heading, or leave that index's row showing |\n| `content.tabs.link` | same-titled tabs sync within a page, not across pages |\n| `content.action.edit` / `.view` | `github` |\n| `announce.dismiss` | `banner.dismissible`; the text is `main.html`'s `{% block announce %}` → `banner.content` |\n| `content.code.annotate` | no equivalent (the codemod reports annotation markers) |\n| `content.code.copy`/`.select`, `navigation.footer`/`.path`/`.top`/`.instant*`/`.tracking`/`.prune`, `toc.*`, `search.*`, `header.autohide`, `content.tooltips` | drop — built in or no equivalent. Blume's copy button copies only the commands of a `console` or `shellsession` block, without prompts or output, so a custom script that stripped them (`extra_javascript`) can go once those blocks use one of the two |\n\n## Navigation\n\nBlume builds the sidebar from folders (SKILL.md \"Navigation is the file tree\"). Translate `nav` into folders and `meta.ts` (the codemod already set each page's `title`/`sidebar.label`):\n\n- **`- Section: [ … ]`** → a folder; a label that differs from the humanized folder name (\"Getting started\" vs \"Getting Started\") → `meta.ts` `title`. List order → `meta.ts` `pages`, every child. Root order → the content root's `meta.ts`.\n- **A section holding an `index.md`** (with `navigation.indexes`) has that page as its own → nothing to do (see `navigation.indexes` above). Without the feature, Material lists the index as an ordinary row, which is what Blume does anyway.\n- **Nesting that doesn't match folders** is SKILL.md's \"Config-declared nesting\". Decide per section and report:\n  - **One flat folder split into subsections** (`Advanced:` listing `guides/webhooks.md`): a **`(group)/` folder** first, as SKILL.md says — `guides/(advanced)/webhooks.md` keeps `/guides/webhooks` and adds the sidebar group, with no redirects.\n  - **A folder shown under another section** (uv's `pip/` under \"Concepts\"), where a group folder would change the URL segment: **move the files and pin `slug: <old route>` on each moved page**. The sidebar follows the file and the URL follows the slug, with no redirects and no explicit sidebar. Use it when the source kept URLs on purpose.\n  - Otherwise, move the files and add `redirects` (the URLs change), or declare the shape in an explicit `navigation.sidebar` (it replaces the whole generated tree).\n  - Either move puts a page a folder deeper: re-base its relative links and assets, and rewrite every link to it to name the file as it now is (a link to `page.md` resolves to a slugged page only when it names that exact file).\n- **`- Title: https://…`** → `navigation.featured` (top level) or `navigation.actions`; a `nav` entry to a non-page file (`llms.txt`, a PDF) → a featured link.\n- **Pages left out of `nav`** → `sidebar.hidden: true` (note: this also drops them from `sitemap.xml`).\n- **No `nav`**: MkDocs lists each folder's pages (`index` first, then by filename) before its subfolders, labeled \"Getting started\"-style; Blume sorts by title. Add `meta.ts` `pages`/`title` where that differs.\n- **`awesome-pages` `.pages` / `awesome-nav` `.nav.yml`** → a `meta.ts` per folder: `title` → `title`; `nav` → `pages` (drop awesome-pages' `...` and awesome-nav's glob entries like `\"*\"`; an awesome-pages `nav` without `...` hides everything unlisted → `sidebar.hidden: true`; `- Title: page.md` → `sidebar.label`); `hide: true` → hide its pages; sorting, `append_unmatched`, and `collapse*` options drop (report). Delete the files.\n- **`literate-nav`** (`SUMMARY.md`): its nested link list is the nav — convert it like `nav` and delete it (it would publish as a page).\n\n## URLs and redirects\n\nSave the old URL list first — every `<loc>` in the old `site/sitemap.xml` (or the live one) plus every `redirect_maps` key — and check it at the end.\n\n| Source file | MkDocs URL (`use_directory_urls: true`) | (`false`) | Blume route |\n| --- | --- | --- | --- |\n| `docs/a/b.md` | `/a/b/` | `/a/b.html` | `/a/b` |\n| `docs/a/index.md` | `/a/` | `/a/index.html` | `/a` |\n| `docs/a/README.md` | `/a/` | `/a/index.html` | `/a/README` until renamed `index.md` (the codemod does) |\n| `docs/01-setup.md` | `/01-setup/` | `/01-setup.html` | **`/setup`** — Blume strips numeric prefixes; redirect, or pin `slug: 01-setup` |\n\n- **Trailing slashes**: a static build writes `<route>/index.html`, so slashed URLs keep working on static hosts; `vercel()` 308s them; Cloudflare Workers needs `html_handling: \"drop-trailing-slash\"`. `blume dev` and `blume preview` redirect a slashed URL to the slashless page, so old URLs can be checked with or without the slash. **Write targets in any client-side script** (anchor-redirect maps) slashless: Blume's URLs have none.\n- **`use_directory_urls: false`**: one exact redirect per non-index page (`{ from: \"/a/b.html\", to: \"/a/b\" }`; a pattern can't strip a suffix). Hosts that read `_redirects`/`vercel.json` answer them; GitHub Pages, which reads neither, is unverified.\n- **`redirects` plugin**: each `redirect_maps` key and value is a Markdown path relative to `docs_dir` → `{ from: <route of key>, to: <route of value> }`, `#anchor` kept; an `http(s)` value stays a URL.\n- **Anchor-level redirects** (a script mapping `/old/#section`): servers never see fragments. Keep the script (`script({ src })`, targets slashless) or drop it; report either way.\n- **Heading ids**: MkDocs' default slugify strips accents, collapses punctuation runs (`A & B` → `a-b`), and numbers duplicates `_1`; Blume keeps accents, writes `a--b`, and numbers `-1`. Run `pin-heading-ids.mjs` (above) — don't compare by hand.\n- **Links**: the codemod rewrites links to renamed files. After you move or rename anything yourself, point links at the real filenames again — a `page.md` link to a file now named `page.mdx` still lands on its page (Blume tries the other extension, `slug` included), but breaks wherever the Markdown is read as files, such as on GitHub. Links inside an included partial resolve from the **including** page, as with MkDocs snippets. A link to a file moved into `public/` becomes root-relative. A link MkDocs couldn't map to a file (`../install/`, written against MkDocs' slashed URLs) lands one level higher in Blume on any page that isn't an `index`, because Blume reads relative links from the page's folder: point it at the `.md` file. `blume validate --strict` catches the rest. The codemod percent-encodes angle-bracket destinations (`[x](<https://…(…)>)` → `…%28…%29`), which only tidies: Blume reads both forms, and rebases a partial's `<…>` image path onto the including page.\n- **llms.txt plugins**: Blume writes `/llms.txt` and `/llms-full.txt`. The plugin's `/<route>/index.md` copies become `/<route>.md`: add one exact redirect each (a pattern can't map them; `blume audit --only redirects` accepts `.md` targets), and reword any URL advice carried into `agents.llmsTxt.details`.\n\n## Which pages become `.mdx`\n\nDirectives, components, math, and Mermaid work only in `.mdx`, and so does an `<include>` indented inside a list item (a `.md` page reads it as indented code and ships the bare tag); the codemod renames exactly the pages that use them (or include a partial that does) and keeps the rest `.md`. MDX **fails the build** on Python-Markdown-valid source, and the codemod reports what's left in renamed pages: `<!-- -->` comments (→ `{/* … */}` on **one line**), `<https://…>` autolinks, unclosed `<br>`/`<img>`, `<placeholder>` text in prose (`uv run <command>` → backticks), and any `{…}` in prose — attribute lists, critic markup, Jinja, `{name}`. Most fail to compile, and `blume check` (and `validate`, `dev`, `build`) names each at its line as `BLUME_MDX_SYNTAX`; it also names an attribute list (`BLUME_MDX_ATTRIBUTE_LIST`: move the attributes onto a JSX element, or drop them) and an unclosed void tag (`BLUME_MDX_UNCLOSED_ELEMENT`: close it, `<br />`). `{ width=\"300\" }`, `{target=\"_blank\"}`, and `{name}` compile and then fail the render, which `blume build` reports as `BLUME_MDX_UNDEFINED_NAME` at the expression's line. In a `.md` page none of these fail and no extension syntax renders: it ships as text (`--8<--` becomes \"–8<–\", `*[HTML]: …` shows verbatim), and an attribute list warns `BLUME_MD_ATTRIBUTE_LIST`. Smart punctuation turns prose `--flag` outside backticks into an en dash.\n\n## Content\n\nWhat the codemod converts, for checking its output, and what it leaves for you:\n\n| MkDocs | Blume |\n| --- | --- |\n| `!!! type \"Title\"` (body indented 4) | `:::type[Title]` … `:::` (longer `::::` outside nested ones). Material styles 12 types; others render like `note`. Mapped: `abstract`/`question` → `info`, `failure`/`bug` → `danger`, `example`/`important`/`todo` → `note`, `caution`/`attention` → `warning`, `hint` → `tip`, `quote` → a blockquote. Untitled callouts lose Material's \"Note\" title bar; a bracket title keeps its inline Markdown (an unbalanced `]` flattens it) |\n| `??? type \"Title\"` / `???+` | `<Expandable title=\"Title\">` / `defaultOpen`; the type's color drops, and the title keeps its inline Markdown (the codemod strips raw HTML, which would show as text) |\n| `=== \"Tab\"` | every tab one code block → `<CodeGroup>` with the label as `title=\"…\"`; otherwise `<Tabs>`/`<Tab title>` (`===+` → `defaultTabIndex`, `===!` → a new group) |\n| `hl_lines=\"2 4-5\"`, `linenums=\"1\"`, `{ .py title=\"x\" }`, options without a language | `{2,4-5}`, `lineNumbers` (a start other than 1 drops), `py title=\"x\"`, `text {…}`. One left over warns `BLUME_CODE_FENCE_OPTION` (with the Blume spelling) or `BLUME_UNKNOWN_CODE_LANGUAGE` |\n| `pycon`, `pwsh-session`, `extend_pygments_lang` names | `python`, `powershell`, the mapped lexer (any other name Shiki lacks warns `BLUME_UNKNOWN_CODE_LANGUAGE`: map it or write `text`) |\n| `` `#!python x()` `` | `` `x(){:python}` `` |\n| `{ #id }`, `{: #id }`, `{#id}` on headings, also beside classes (`{ #id .wide }`) | `[#id]`, classes dropped (a self-link like `### [x](#x) {: #x }` drops). One left over warns `BLUME_MD_CURLY_ANCHOR` in `.md` (only `{#id}` pins there), and `BLUME_MDX_CURLY_ANCHOR` in `.mdx` (`BLUME_MDX_ATTRIBUTE_LIST` beside classes): write `[#id]` |\n| other attribute lists | dropped and reported (one left over warns `BLUME_MD_ATTRIBUTE_LIST` in `.md`): buttons → links or `<Card>`s (a homepage CTA → `navigation.cta`); image sizing → `<img … />` or `<Frame>`; `{target=\"_blank\"}` → `markdown.externalLinks: true` |\n| `++ctrl+c++`, `==x==`, `^^x^^` | `<kbd>Ctrl</kbd>+<kbd>C</kbd>`, `<mark>`, `<ins>` |\n| `:material-check:` (mapped icons) | `<Icon icon=\"check\" />`; `:octicons-link-external-16:` drops (see `externalLinks`) |\n| `\\(x\\)`, `\\[`…`\\]` | `$$x$$`, `$$` blocks |\n| `*[HTML]: …`, `[TOC]` | removed |\n| `<div markdown>` | attribute dropped (Markdown inside renders in `.mdx`) |\n| Body `# H1` | frontmatter `title` |\n| **By hand:** single-`$` math | `$$…$$` inside the sentence, where it is math |\n| definition lists | `<dl>` or a list |\n| `///` blocks (pymdownx.blocks) | the same targets as `!!!`/`???`/`===`; `/// caption` → `<Frame caption>` |\n| grid cards (`<div class=\"grid cards\" markdown>`) | `<CardGroup>`/`<Card>` |\n| code annotations (`# (1)!` + a list) | prose after the block |\n| `![](x#only-light)` / `#only-dark` | a `theme.css` rule: `:root:not([data-theme=\"dark\"]) img[src$=\"#only-dark\"], :root[data-theme=\"dark\"] img[src$=\"#only-light\"] { display: none; }` (checked in a browser) |\n| emoji `:smile:`, magiclink `#123`/`@user` | paste the emoji; write the links |\n| unmapped icons | a Lucide name in `<Icon>` (verify at lucide.dev), or drop; brand icons (Python, Linux, Docker) have none |\n\n### Snippets and includes\n\n- Snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), never the page; the codemod rewrites them to `/`-rooted `<include>`s.\n- Includes must live in the content root. The codemod copies outside files (`CONTRIBUTING.md`, `examples/`) into `_snippets/`; if the source keeps changing, ship a small `predev`/`prebuild` script that re-copies it, and gitignore the copy. Drop a copied file's H1 (the codemod reports it) — the page's title already renders one.\n- Line ranges (`file.py:3:10`) have no `<include>` equivalent: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (`lines`, run before every build so it keeps tracking the source). Sections (`file.md:name`, marked `--8<-- [start:name]`) aren't read by that script: split the section into its own file and report it.\n- `auto_append` link-definition files: `<include>` the file at the end of each page that uses them. An included file's definitions resolve references in the including page, as if written inline (and the page's resolve in the partial).\n- A partial is parsed with the page that includes it: its directives render only in an `.mdx` page. An HTML comment in a `.md` partial stays hidden in an `.mdx` page too.\n- A snippet file that isn't under a `_` or `.` folder or name is a page too, and publishes on its own as it did in MkDocs; the codemod reports each one. Move it under `_snippets/` unless it should stay a page.\n- `include-markdown` (`{% include \"./x.md\" %}`): `./` paths are page-relative like Blume's, bare ones `docs_dir`-relative (→ `/x.md`); `start`/`end` excerpts → their own file.\n\n## Frontmatter\n\nThe codemod maps `hide: [toc]` → `mode: wide`, `hide: [navigation]` → `mode: center`, `hide: [footer]` → `pagination: false`, `status: new` → `sidebar.badge: New` (`deprecated` → `deprecated: true`), `tags` → `search.tags` (a hosted-search facet; tag pages drop), `icon: material/…` → a Lucide name, and drops `subtitle`, `template`, `social`, `comments`, `render_macros`. `search.exclude`/`boost` keep their meaning. It reports every other key Blume's strict schema rejects (`BLUME_FRONTMATTER_INVALID`) — map or remove them; `.meta.yml` defaults (the `meta` plugin) apply to every page in their folder.\n\n## Plugins — map, don't drop\n\n| Plugin | Blume |\n| --- | --- |\n| `search` | built in — delete |\n| `redirects` | `redirects` (URLs) |\n| `git-revision-date(-localized)` | `lastModified: \"git\"` (it shows a visible \"Last updated\" line) |\n| `git-committers`, `git-authors`, `minify`, `privacy`, `offline`, `typeset`, `info` | drop |\n| `social`, `glightbox`, `optimize` | built in: Open Graph images, image zoom, image optimization |\n| `group` | unwrap and map its plugins |\n| `exclude` | `content.exclude` |\n| `meta`, `tags` | Frontmatter |\n| `blog`, `rss` | Blog |\n| `awesome-pages`, `awesome-nav`, `literate-nav`, `section-index` | Navigation |\n| `include-markdown` | Snippets |\n| `llmstxt` | built in: `markdown_description` → `agents.llmsTxt.details` (reword URL advice); `sections` drop |\n| `macros`, `mkdocstrings`, `mkdocs-click`, `gen-files`, `mkdocs-jupyter` | Manual decisions |\n| `i18n`/`static-i18n`, `mike` | i18n; Versioning |\n| anything else | read what it does; report it |\n\n## Manual decisions — stop and report\n\n- **mkdocstrings** (`::: package.module`): no Python API generator in Blume. Keep that reference published elsewhere and link to it, use `openapi()` for an HTTP API, or hand-write the key pages; redirect the URLs you drop. The same for `mkdocs-click` CLI references and `gen-files` output.\n- **macros**: plain values (`extra`, `include_yaml`, `{% set %}`) → `variables`, same `{{ name }}` syntax — names take letters, digits, `_`, `-` only, and `{{ config.x }}`/`{{ page.x }}` must be inlined. `{% include 'x.md' %}` (from `include_dir`, default `docs_dir`) → `<include>/x.md</include>`. Macro calls and control flow → expand into plain content (a one-off render script is fine); strip `{% raw %}` wrappers.\n- **Template overrides** (`custom_dir`) and **hooks**: read each. Announcement → `banner`; `<head>` meta tags → `seo.metatags` (minus the tags Blume owns); analytics partials → adapters; `<title>`/JSON-LD tweaks → Blume's own; the rest → layout slots (`defineComponents`) or `blume eject`.\n- **Notebooks** (`mkdocs-jupyter`): export to Markdown or link out.\n\n## i18n\n\n`static-i18n` `languages` → `i18n: { defaultLocale, locales: [{ code, label }] }` (both serve the default at the root, others under `/<locale>/`). `docs_structure: suffix` (`page.fr.md`, the default) → `parser: \"dot\"`; `folder` → the default `dir` parser with the **default locale's folder moved up to the content root**. `nav_translations` → per-locale `meta.ts` titles; `fallback_to_default` matches `fallbackLocale`. Pin translated headings with `[#id]`.\n\n## Versioning (`mike`)\n\nmike publishes built HTML per version (`/<version>/…`, aliases like `/latest/`); old versions' sources live only in git tags. Migrate the current version unless asked: serve it at the root and redirect `/<alias>/:path*` and `/<version>/:path*` to `/:path*`. Older versions stay on the old host (a `navigation.featured` link) or are converted from their tags into Blume's `versions.archived` (ids start with a letter). Report which.\n\n## Blog (Material `blog` plugin)\n\nPosts in `blog/posts/` → `type: blog` pages. `date: { created, updated }` → `date` + `lastModified`; `authors: [id]` → inline objects from `.authors.yml`; `categories` → `search.tags` or drop. Keep the old dated URL with a full-route `slug: blog/2024/01/31/my-post`, or redirect. Delete `<!-- more -->` in `.mdx`. Blume has no blog index, archive, category, or pagination pages: write `blog/index.mdx` (`docs/advanced/blog.mdx`), report the rest. RSS moves to `/blog/rss.xml`; with the `rss` plugin (mkdocs-rss-plugin), redirect its `/feed_rss_created.xml` and `/feed_rss_updated.xml` (and `/feed_json_*.json`) there.\n\n## Repo integration and teardown\n\n- **Assets**: everything non-Markdown in `docs_dir` that's served by URL — logo, favicon, downloads, `CNAME`, verification files — moves to `public/`. Images and other files that pages link relatively can stay: Blume publishes each with the page, though at a new URL, so a download whose old URL is linked from elsewhere moves to `public/` too.\n- **Renamed files**: after the codemod, **grep the whole repo for the old page paths** — release tooling that bumps versions inside pages (`[tool.rooster] version_files`, `bumpversion`), Renovate/Dependabot file globs, CODEOWNERS, `.prettierignore`, CI path filters, and scripts.\n- **Prettier**: `.prettierrc` and `.editorconfig` overrides keyed on `*.md` stop applying to the new `.mdx` files — extend them to `*.mdx` before reformatting. Prettier turns a multi-line `{/* … */}` into `{/_ … _/}` (keep MDX comments on one line) and rewrites a URL containing parentheses as a `<…>` destination (fine in a page; percent-encode it in a partial, see Links). If the repo uses Ultracite/oxfmt, see `references/monorepo.md` §6.\n- **CI**: replace `mkdocs build`/`gh-deploy` with Node 22, an install, `blume build`, and `blume validate --strict` (GitHub Pages: the \"Deploy Markdown docs to GitHub Pages\" guide); a `.readthedocs.yaml` build is a hosting change to report. Add `.gitignore` entries for `node_modules/`, `.blume/`, `dist/`, and `.env.local`.\n- **Teardown**: delete `mkdocs.yml` (and `INHERIT`ed bases), `overrides/`, `.pages`/`.nav.yml`/`SUMMARY.md`/`.meta.yml`, hooks, and `extra_css`/`extra_javascript` files once ported. Leave the Python docs dependencies for a follow-up: removing a `pyproject.toml` docs group needs a lock regeneration (`uv lock`), and CI running with `--locked`/`UV_LOCKED=1` fails until it's done — report it.\n\n## Leftover check\n\nRerun until it prints only what you mean to keep (code samples, and `{{name}}` variables you defined):\n\n```bash\ngrep -rnE '^\\s*(!!!|\\?\\?\\?\\+?|===[+!]*) |^\\s*/{3,} ?[a-z]|--8<--|\\{ *[.#:][^}]*\\}|\\{\\{|\\{%|<!--|:(material|octicons|fontawesome|simple)-[a-z0-9-]+:|\\+\\+[a-z]|==[^= ][^=]*==|\\{\\+\\+|\\{--|^\\*\\[|`#![a-z]|hl_lines=|linenums=' docs\n```\n\n## Dropped — report these\n\nTemplate overrides and hooks with no slot equivalent, links inside `copyright`, code annotations, admonition title bars and collapsible callout colors, `inline` admonition placement, `palette.accent`, unmapped and brand icons, abbreviation tooltips, definition-list styling, snippet line ranges and sections (split into files), tag index pages, blog archive/category/pagination pages, anchor-level redirects without the script, cross-page tab memory, git committers, mkdocstrings/click/gen-files/notebook output not carried over, macro functions and Jinja logic, social platforms `footer.socials` lacks, a `linenums` start offset, image sizing from attribute lists, and every plugin you removed without an equivalent.\n",".agents/skills/blume-migrate/references/monorepo.md":"# Migrating into a real repo (monorepos, pnpm, Vercel)\n\nThe framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.\n\n**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.\n\n---\n\n## 1. Non-standard content layout → scope `content.root` + `include`\n\n`content.root` defaults to `\"docs\"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.\n\n**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:\n\n```bash\n# top two path segments of every content file, deduped — your content folders\nfind . -path ./node_modules -prune -o \\( -name '*.md' -o -name '*.mdx' \\) -print \\\n  | sed 's|^\\./||' | cut -d/ -f1 | sort -u\n```\n\nIf that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.\n\n**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: \".\"` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nexport default defineConfig({\n  content: {\n    root: \".\", // content sits directly under this package (apps/docs/), no docs/ subfolder\n    // include/exclude globs are relative to `content.root`. List the actual\n    // content folders so the scan never wanders into src/, public/, etc.\n    include: [\n      \"api/**/*.{md,mdx}\",\n      \"getting-started/**/*.{md,mdx}\",\n      \"guides/**/*.{md,mdx}\",\n    ],\n    exclude: [\"**/node_modules/**\"], // added to the default \"**/_*\" and \"**/.*\"\n  },\n});\n```\n\nSchema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `[\"**/*.{md,mdx}\"]`), **`content.exclude`** (array of globs, relative to `content.root`, added to the default `[\"**/_*\", \"**/.*\"]`; a `!`-prefixed default, `\"!**/_*\"`, takes it back out). Singular `include`/`exclude`, both arrays. A folder `meta.ts` outside every `include` glob is never read (`BLUME_META_OUTSIDE_INCLUDE` names it), so a folder that holds only a `meta.ts`, like an API reference's tag folder, needs a glob of its own (`\"api-reference/**/*.{md,mdx}\"`).\n\nReport to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.\n\n---\n\n## 2. Release-age guards (pnpm `minimumReleaseAge`, Yarn `npmMinimalAgeGate`) block installing fresh Blume\n\npnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.\n\n**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.\n\n**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:\n\n```bash\ngrep -rn \"minimumReleaseAge\\|minimum-release-age\" pnpm-workspace.yaml .npmrc 2>/dev/null\n```\n\n**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):\n\n```yaml\nminimumReleaseAge: 1440\nminimumReleaseAgeExclude:\n  - blume\n```\n\n**Fix — `.npmrc`** form (if the policy lives there instead):\n\n```ini\nminimum-release-age=1440\nminimum-release-age-exclude[]=blume\n```\n\nReport: \"Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else.\"\n\n**Yarn ≥4.10** has the same guard: `npmMinimalAgeGate` in `.yarnrc.yml` (e.g. `2w`). Its per-package exemption is `npmPreapprovedPackages`. Yarn reads rc files from parent directories, so a nested `docs/` package inherits the root's gate. Detect with `grep -rn \"npmMinimalAgeGate\" .yarnrc.yml docs/.yarnrc.yml 2>/dev/null`, then add `npmPreapprovedPackages: [\"blume\"]` (or append `blume` to the existing list) in the file that sets the gate. Don't remove the gate.\n\n---\n\n## 3. Frozen lockfile — regenerate and commit the lockfile in the same change\n\nCI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.\n\n**After every dependency edit, regenerate the lockfile with the repo's package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, `bun install`) and stage it with the manifest.** The pnpm case:\n\n```bash\npnpm install            # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml\ngit add package.json apps/docs/package.json pnpm-lock.yaml\n```\n\nRules:\n\n- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.\n- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.\n- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors \"lockfile not up to date,\" you forgot the plain install.\n\n---\n\n## 4. Vercel monorepo recipe (root-aware install + build)\n\nA plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/` (for a static build; see d) for `vercel()`). `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.\n\nThis is a **copyable template**, not prose. Three pieces:\n\n**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"blume dev\",\n    \"build\": \"blume build\",\n    \"preview\": \"blume preview\"\n  },\n  \"dependencies\": {\n    \"blume\": \"^2\"\n  }\n}\n```\n\n**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"framework\": null,\n  \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\",\n  \"buildCommand\": \"pnpm run build\",\n  \"outputDirectory\": \"dist\"\n}\n```\n\n**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**\n\n- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.\n- **Node version** = 22.19 or newer (Blume requires it). Or pin it in the docs package: `\"engines\": { \"node\": \">=22.19\" }`.\n\nHow the pieces fit, with Root Directory = `apps/docs`:\n\n- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.\n- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.\n- `outputDirectory: \"dist\"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.\n- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.\n\nIf the repo uses **Turborepo**, you can instead build through the root pipeline — `\"buildCommand\": \"cd ../.. && pnpm turbo build --filter=docs\"` — for remote-cache reuse. Keep `outputDirectory: \"dist\"` and Root Directory `apps/docs`.\n\nReport every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.\n\n**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)\n\n**d) Server output (`deployment: vercel()`).** A `vercel()` build (needed for the assistant, the MCP server, Mixedbread search, or the playground proxy) doesn't leave a static `dist/` for Vercel to serve. It writes the Build Output API folder `.vercel/output` in the docs package, which Vercel deploys as is. Use the same `vercel.json` without `outputDirectory`: `{ \"$schema\": …, \"framework\": null, \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\", \"buildCommand\": \"pnpm run build\" }`.\n\n- Keep `framework: null`: it overrides a preset the project already has (Nuxt, Next.js), whose build settings would replace these.\n- Root Directory and Node are the same dashboard settings. Server secrets (`AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token) go in the project's environment variables.\n- With Turborepo, add `\".vercel/output/**\"` to the docs task's `outputs`.\n- The redirect caveat above doesn't apply: `vercel()` writes redirects into `.vercel/output/config.json`, so they're real 3xx responses.\n- `blume preview` can't serve this build; check it with `blume dev` and a preview deployment.\n\n---\n\n## 5. Astro/Vite version mismatch → pnpm patch (manual)\n\nBlume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.\n\n**Detect the risk factor** before it bites:\n\n```bash\ngrep -rn \"\\\"vite\\\"\\|vite@\\| vite:\" pnpm-workspace.yaml package.json 2>/dev/null | grep -i \"override\\|resolution\\|catalog\\|vite\"\n```\n\nBlume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):\n\n```bash\n# 1. Open the offending version in an editable temp dir (use the exact version\n#    pnpm resolved — check `pnpm why astro`):\npnpm patch astro@7.0.6\n\n# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface\n#    (the exact edit depends on the mismatch; the crash's stack trace names the\n#    file and call site). Then commit the patch:\npnpm patch-commit '<the temp path pnpm printed>'\n```\n\n`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  astro@7.0.6: patches/astro@7.0.6.patch\n```\n\nCommit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.\n\nThis is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.\n\n---\n\n## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)\n\nIf the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, titled fences like `:::warning[Heads up]`, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.\n\nThe fix is a committed **pnpm patch** (`patches/oxfmt@0.71.0.patch`), shipped with this skill at `assets/oxfmt@0.71.0.patch` — the same diff Blume's own repo applies, covering titled fences as well as bare ones. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:\n\n```bash\n# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;\n#    <skill> = this skill's directory, the one containing SKILL.md):\nmkdir -p patches\ncp \"<skill>/assets/oxfmt@0.71.0.patch\" patches/oxfmt@0.71.0.patch\n```\n\nThen register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  oxfmt@0.71.0: patches/oxfmt@0.71.0.patch\n```\n\nCommit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.\n\n- **The patch is pinned to `oxfmt@0.71.0`** (the version Blume's own repo pins). pnpm requires an exact version match, and the diff targets a file whose name is hashed per oxfmt release (`dist/markdown-*.js`), so it won't apply to any other version. If `pnpm why oxfmt` reports a different one, pin `oxfmt` to `0.71.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.59 as a peer). If the repo can't take that pin, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.\n- **The risk isn't Ultracite-only.** Any plain `oxfmt` run over Markdown (a lint-staged `oxfmt` on `**/*.md`, say) treats `:::` the same way. Check `lint-staged`/pre-commit config for globs that reach the migrated pages; `.mdx` files outside an `*.md` glob are safe.\n- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.\n\n---\n\n## Checklist for a workspace migration\n\n- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.\n- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.\n- [ ] `npmMinimalAgeGate` present? Added `blume` to `npmPreapprovedPackages` only.\n- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.\n- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22.19+ in the Vercel project.\n- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.\n- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.71.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.\n- [ ] Reported every repo-specific edit and every manual step left to the user.\n",".agents/skills/blume-migrate/references/nextra.md":"# Nextra → Blume\n\nNextra (on Next.js) declares navigation and per-page labels in `_meta` files — and crucially, **a folder's title and its pages' sidebar labels live in the _parent_ `_meta`**. Reconciling that cross-file inheritance is the main task. Nextra 4 was a near-rewrite, so **fingerprint the generation first** — it decides where config lives, where content lives, and what the components are called.\n\n## Detect & fingerprint the generation\n\n- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).\n- **v2/v3 (Pages Router):** content in `pages/`, `_meta.json` (v2) or `_meta.{js,ts}` (v3), a `theme.config.{js,jsx,ts,tsx}`, components from `nextra-theme-docs` (v2) or `nextra/components` (v3).\n- **v4 (App Router):** **no `theme.config`** — config lives as component props in `app/layout.{jsx,tsx}` (`<Layout>`, `<Navbar>`, `<Banner>`, `<Footer>`, `<Search>`). Content is either a **`content/`** directory (with a catch-all `app/[[...mdxPath]]/page.jsx` gateway) **or** Next page files: **`app/**/page.mdx`**.\n- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.\n\n**v4 app-directory repos need a restructure, not a `content.root`.** Pointing `content.root` at `app/` gives every route a trailing `/page` and scans `layout.jsx`/gateway files. Move each `app/foo/page.mdx` → `foo.mdx` (or `foo/index.mdx` when it has children), and exclude the non-MDX app files. The catch-all `page.jsx`, `mdx-components.*`, and `app/layout.*` are teardown artifacts — but **harvest `app/layout.*` first** (see Config).\n\n## Config\n\nNone of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:\n\n- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the footer's repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`; if its origin is not `https://github.com`, set `github.host` to that origin — with `host` omitted, Blume builds the repository and edit links against public GitHub); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer.text` (v2) / `footer.content` (v3) → `footer.copyright` when it's a copyright line: reduce the JSX to plain text, and a `{new Date().getFullYear()}` becomes `${new Date().getFullYear()}` in a template literal; anything else in it → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.\n- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>` (its children are v3's `footer.content`). Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).\n- **`next.config.*`:** harvest **`redirects()`** — static entries and `:param`/`:path*` patterns become Blume `redirects: [{ from, to }]` as written (`permanent: true` → `status: 308`, `false` → `307`); ones with regex params (`:slug(\\d+)`) or `has`/`missing` conditions move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.\n- Root `_meta` entries with **`type: \"page\"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: \"page\"` only maps at the **root**.\n\n## Navigation: `_meta` → `meta.ts` + frontmatter\n\n**Every `_meta` file must be carried over — always convert it to `meta.ts` + frontmatter, never drop it in favor of filename inference.** `_meta` is Nextra's canonical nav source (ordering, labels, folder titles); the filesystem alone can't reproduce it.\n\nFor each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):\n\n| Nextra `_meta` entry | Blume |\n| --- | --- |\n| ordinary page/folder (string or `{ title }`) | slug → parent `meta.ts` `pages` (ordering). A **page** title → that page's frontmatter `sidebar.label`. A **folder** title → that child folder's `meta.ts` `title` (title lives in the parent!). A JSX title → extract its text. |\n| `display: \"hidden\"` | frontmatter `sidebar.hidden: true` |\n| `type: \"separator\"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |\n| `type: \"menu\"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |\n| `href` (external link) | root-level → **`navigation.actions`** (`[{ label, href }]`, plain header links, matching Nextra's navbar placement) or **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route, survives on phones); only drop deep-nested ones (report) |\n| `type: \"page\"` (subfolder, not root) | drop (only root → tabs) |\n| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |\n| `theme: { layout: \"full\" }` on a page | that page's frontmatter `mode: wide` (full column width, no table of contents — the closest match; report) |\n| other `theme` keys (`sidebar: false`, `toc: false`, `typesetting`, …) | drop (report — no per-page switches for them) |\n| `\"*\"` wildcard entry | apply its value as the default for unlisted siblings, then drop |\n\nWrite each folder's `meta.ts` with its `pages` order (from its own `_meta`) and its `title` (inherited from the parent's `_meta`). A v4 **`_meta.global.*`** file holds the whole tree in one place (folder entries carry `items`) — split it into per-folder `meta.ts` files with the same rules.\n\n**Keep the order with `display: \"group\"`.** Nextra draws folders as collapsible rows and keeps pages and folders interleaved as `_meta` lists them. Blume's default `flat` display draws a folder as a plain section header, so it lists each level's pages above its subfolders — a page `_meta` puts after a folder jumps above it. Set `navigation.sidebar.display: \"group\"` (or `display: \"group\"` in the `meta.ts` of every subfolder at a level whose order must hold — one flat sibling folder reorders the level) to keep the authored order in nested folders. Groups then start closed unless they hold the current page, while Nextra opens every folder shallower than its `defaultMenuCollapseLevel` (default `2`: the top-level folders), so add `collapsed: false` to those folders' `meta.ts`. The top level (and each tab's top level) lists loose pages before folders in every display mode, so a root `_meta` that puts a page after a folder can't be matched — report it.\n\n## Frontmatter & titles — synthesize, don't just pass through\n\nMost Nextra pages have **no frontmatter**. Nextra keeps two titles apart: the **sidebar label** falls back `_meta` title → `sidebarTitle` → `title` → first body H1 → filename, while the **heading readers see** is the body H1. Blume renders frontmatter `title` as the page H1, so for **every page**: set frontmatter `title` from the body H1 (else frontmatter `title`, else the `_meta` title, else a humanized filename), **remove the body H1**, and set `sidebar.label` to the `_meta` title (else `sidebarTitle`) wherever it differs from `title`. Don't take the `_meta` label as `title` — that renames the page's heading. `asIndexPage: true` (v4 folder-index marker) → make the file the folder's `index.mdx` and drop the key. Drop any other non-schema key and report it.\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → directive. Types: `default` (the prop default) → `:::note`, `info`→`:::info`, `warning`→`:::warning`, `error`→`:::danger`, `important` (v4) → `:::note`. Bare `<Callout>` → `:::note`. Drop `emoji` (there is no `title` prop to carry — Nextra callouts have none).\n- **v2 flat names:** v2 exported `Tab` and `Card` (flat) from **`nextra-theme-docs`**; v3/v4 use `Tabs.Tab`/`Cards.Card` from `nextra/components`. Handle both: `<Cards>`/`<Cards.Card>`/`<Card>` → `<CardGroup>`/`<Card>`; `<Tabs items={[…]}>` + `<Tabs.Tab>`/`<Tab>` → `<Tabs>`/`<Tab title=\"…\">` (move labels from the parent `items` onto each `<Tab>`). `Cards` `num` → `CardGroup` `cols`.\n- **`<Steps>`:** Nextra's wraps **Markdown headings (h2–h6)**, one step per heading. Convert each heading to a `<Step title=\"…\">` child (Blume `<Steps>`/`<Step>`) and delete the heading — this also keeps step titles out of the TOC, matching Nextra's behavior.\n- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).\n- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.\n- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` pass through in `.mdx`, where Blume renders them as note/tip/warning/note/danger callouts. In a `.md` page they stay plain quotes (`BLUME_MD_GITHUB_ALERT`): rename it to `.mdx`.\n- **Strip or convert every import:** `nextra`, `nextra/*`, `nextra-theme-docs`, `nextra-theme-blog`, plus `next/image` (→ Markdown image or `<Frame>`), `next/link` (→ plain link), and local components (port or inline; report).\n\n## Code fences\n\nNextra's fence meta differs from Blume's — rewrite it: `filename=\"app.js\"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags either one left); line highlighting `{1,4-5}` carries over unchanged; **drop** word-highlight `/word/` and `copy`/`copy=false`. **Keep** inline-code `{:lang}` suffixes (`` `useState(){:js}` ``) — Blume highlights them natively. ` ```sh npm2yarn ` fences → ` ```package-install `.\n\n## Math\n\nNextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume, block math `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field). Convert Nextra's ` ```math ` fences → `$$…$$` blocks (they render as plain code blocks otherwise). Inline math is `$$…$$` too: a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence. MathJax-specific macros → report.\n\n## i18n\n\n- **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → match Blume's `dot` parser as-is, default-locale suffix included: `i18n: { defaultLocale, locales: [{ code, label }], parser: \"dot\" }`, no file moves. Page titles land in each file's own frontmatter, so they stay per-locale, but under `dot` a folder's `meta.ts` serves every locale — build it from the default locale's `_meta` and report translated folder titles from the other locales' `_meta` files.\n- **v4:** every locale has a `content/<lang>/` dir, the default included, and Nextra serves every locale under its prefix (`/en/…`). Blume's `dir` parser takes the other locales' dirs as-is, but the default locale lives **at the content root**: a `content/en/` dir is ordinary content that publishes at `/en/…` and warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`. Move the default locale's files (and `_meta`-derived `meta.ts` files) up one level, and set `i18n.hideDefaultLocalePrefix: false` to keep serving them at `/en/…` as Nextra did — or keep Blume's unprefixed default and add a `redirects` entry per page.\n\n## Package.json & teardown\n\nRepoint `dev`/`build`/`start` scripts to the Blume CLI; remove `next`/`nextra`/`nextra-theme-*` deps and add `blume`. Delete after harvesting: `next.config.*` (redirects first!), `theme.config.*` (v2/v3), `app/layout.*` + the catch-all gateway + `mdx-components.*` (v4).\n\n## Dropped — report these\n\nFooter content other than a copyright line; `primaryHue`-style theming beyond an accent color; `faviconGlyph`; `_meta` separators, menus, `newWindow`; per-page `theme` switches (and `layout: \"full\"` approximated as `mode: wide`); root-level pages `_meta` ordered after a folder; `<Bleed>`; MathJax macros; word-highlight/copy fence meta; any icon you can't reconstruct as a Lucide name.\n",".agents/skills/blume-migrate/references/readme.md":"# ReadMe → Blume\n\nReadMe (readme.com developer hubs) is a hosted platform. Only content reaches Git: settings, the landing page, the glossary, variables, redirects, and every metrics and login feature live in its dashboard. Pages are MDX, or ReadMe's looser \"MDXish\" dialect, saved as `.md`. Published URLs are flat per section (`/docs/<slug>`, `/reference/<slug>`, `/recipes/<slug>`, `/page/<slug>`, `/changelog/<slug>`) however deep a page sits in the sidebar. The plan below keeps every one of them: section folders stay at the top of the content root, categories become `(group)` folders, and nested pages pin their flat URL with `slug`. The API reference is the exception: Blume generates operation pages at new routes, so each old one gets a redirect.\n\nTwo things go wrong with a green build and no warning: emoji blockquote callouts (plain blockquotes) and `:emoji:` shortcodes (literal text). Two more only warn: `doc:`/`ref:` links (dead links; `blume validate` reports `BLUME_UNSUPPORTED_LINK_SCHEME`) and inline `onClick` handlers (they render and do nothing; `BLUME_MDX_EVENT_HANDLER`). The codemod handles the first three; the handlers are yours.\n\n## Detect\n\n| Shape | Tells | The sidebar lives in |\n| --- | --- | --- |\n| **A. Bi-directional Git sync** (ReadMe Refactored) | `_order.yaml` in `docs/`, `reference/`, `recipes/`, `custom_pages/` and their folders; category folders with spaces (`docs/Getting Started/`); `reference/*.json` specs beside endpoint pages with `api: { file, operationId }` frontmatter; `reference/ReadMeConfig/`; `custom_blocks/`; a `main` branch holding only `changelogs/`; branches named after versions (`v3.0`) | folders and `_order.yaml` |\n| **B. `rdme@10` upload** (one-way) | CI running `readmeio/rdme@v10` or `npx rdme@10` with `docs upload`, `reference upload`, `changelog upload`, `custompages upload`, `openapi upload`; frontmatter `category: { uri }`, `parent: { uri }`, `position`, `privacy: { view }`, `content: { excerpt }` (also `rdme docs export` output) | frontmatter |\n| **C. Legacy `rdme` ≤ 9** | CI running `readmeio/rdme@v9` or older (`rdme docs ./dir`, `rdme openapi`, `rdme changelogs`); frontmatter `category: <24-hex id>` or `categorySlug`, `parentDoc` or `parentDocSlug`, `order`, `hidden`, `type: basic \\| link \\| error` | frontmatter, keyed by opaque IDs |\n\nrdme ignores folder layout, so in shapes B and C the folders say nothing about the sidebar. rdme 10 still accepts `rdme changelogs` as a hidden alias, so tell B from C by the rdme version and the frontmatter. A shape A repo can still run `rdme openapi upload` in CI: it's shape A.\n\n**Get everything before you convert.**\n\n- **Shape A** is complete per version branch. Find the branch holding the hub's default version (it can be `v3.0`, not `main`). **Changelogs live only on `main`**: `git archive <main-sha> changelogs | tar -x -C <work-dir>`. Ignore `<version>_<name>` branches (ReadMe's editing branches, kept after they merge).\n- **Shapes B and C** hold only what CI uploads, and drift: editor changes never come back, and a failing upload step stops changes going out. Inventory the live hub, export what the repo lacks, and diff pages against their live source.\n- **Exports.** A Refactored project: connect bi-directional sync to a new, empty repository (Settings → Git Connection), which writes shape A with every version as a branch; a ZIP from the Branch menu (no images); or `npx rdme@10 docs export <dir> --branch <version>` and `rdme reference export <dir>` (shape B frontmatter). A legacy project: `rdme@9` can't export; read pages through API v1 (`GET /api/v1/categories/{slug}/docs`, `GET /api/v1/docs/{slug}`) or upgrade the project. Refactored hubs usually serve a page's Markdown at `<url>.md` (docs.readme.com does), and `/llms.txt`, when it's on, lists every public page or links per-section files that do.\n\n**Go easy on the live hub.** A ReadMe hub can start answering `429` after as few as eight requests in a few minutes, even spaced seconds apart. Spend them on one page of the hub (save it: it carries the settings JSON below), `sitemap.xml`, `llms.txt`, and a few spot checks. The `files.readme.io` image CDN is separate: one run fetched 84 images there at one every half second without a `429`.\n\n## Run the codemod first\n\nFor shape A, on a branch whose name matches no ReadMe version (see Teardown), with the changelogs copied from `main` into the work tree and one page of the hub saved as `hub.html` (skip the `curl` when the user already saved one at the root):\n\n```bash\ncurl -s https://docs.example.com/docs/<any-page> -o hub.html   # settings JSON + glossary\nnode <skill>/scripts/readme-codemod.mjs --site https://docs.example.com --glossary hub.html .          # report only\nnode <skill>/scripts/readme-codemod.mjs --write --site https://docs.example.com --glossary hub.html .  # apply\n```\n\n`--site` (repeatable: add the `*.readme.io` origin too) makes absolute links to the hub root-relative. It's zero-dependency, deterministic, and idempotent (a rerun reports `Nothing to change.`). It:\n\n- moves `custom_pages/` → `page/`, `changelogs/` → `changelog/`, categories → `(Category)` group folders, `.md` → `.mdx`;\n- maps frontmatter (Frontmatter, below) and pins `slug` wherever Blume's route would differ from ReadMe's URL;\n- converts callouts, links, magic blocks, ReadMe components, variables, reusable content, icons, shortcodes, code fences, and headings (Content);\n- writes `_order.yaml` → `meta.ts`, deletes endpoint, tag, and empty parent pages, and turns link pages into redirects (Navigation);\n- writes `reference/overlays/<spec>.overlay.json` (API reference), `readme-redirects.json` (the redirects it can compute), and `readme-migration.json` (every old URL, every deleted endpoint, for the second pass).\n\n**Its report is the to-do list**: per file, what needs judgment (custom components, inline components, `<style>`, event handlers, dead links, unmapped icons), then \"Left for you\". Work through it with the sections below. A second pass after the first build handles endpoint redirects and order (API reference).\n\nShapes B and C: rebuild the tree first (Navigation, shapes B and C), then run the codemod. With no `_order.yaml` it converts page content and frontmatter only: no moves, slug pins, URL inventory, endpoint handling, or redirects (it reports link pages instead).\n\n## Config: the dashboard → `blume.config.ts`\n\nThere's no config file in Git. Every page of a Refactored hub embeds the project's settings as JSON: read them from the saved `hub.html` rather than asking. Map what's set:\n\n| ReadMe (settings JSON) | Blume | Notes |\n| --- | --- | --- |\n| `name` | `title` |  |\n| `logo`, `logo_white_use` | `logo` | download into `public/`. With `logo_white_use: true` the hub showed a white logo on a colored header, invisible in Blume's light header: recolor the white parts to `currentColor`. A bare mark → the string form (`logo: \"/logo.svg\"`); a wordmark → `{ image: \"/logo.svg\", text: \"\" }` (SKILL.md) |\n| `favicon` | `public/favicon.png` (or `.svg`/`.ico`) | picked up by name |\n| `brand.link_color` (`colors.body_highlight`) | `theme.accent` | `_dark` variants → `{ light, dark }` |\n| `brand.primary_color` (`colors.main`) | `theme.action`, or `theme.accent` when no link color | the colored header has no equivalent: report |\n| `brand.theme` | `theme.mode` |  |\n| `typography.headline` / `body` (`Open+Sans:400:sans-serif`) | `theme.fonts.display` / `body` (`{ name: \"Open Sans\" }`) | Typekit → local `variants`, or drop |\n| `nav_names` + enabled `modules` | `navigation.tabs` | one tab per enabled section with content (`{ label: \"Developer hub\", path: \"/docs\" }`) |\n| header links (`link_url` entries) | `navigation.actions` | a sign-up or \"Get API keys\" button → `navigation.cta`; a login link stays in `actions` |\n| `first_page` | `index.mdx` at the root, or a `/` redirect (status 302) | the landing page doesn't sync |\n| `custom_domain` | `deployment.site` | unless the host is Vercel or Netlify (SKILL.md) |\n| Enterprise group path (`/<project>/docs/…`) | `basePath: \"/<project>\"` | one Blume site per child project |\n| `integrations.google.analytics`, `segment`, `heap`; custom JS | `googleAnalytics()`, `segment()`, `heap()`, or `script({ src \\| content })` in `analytics` | these load only in production builds, and wait for `consent` when it's set |\n| `integrations.intercom`, `zendesk` (a support widget readers click) | a `PageFooter` component (`defineComponents`, SKILL.md \"analytics\") that loads it | not `analytics`, which can hold it until consent. Check region settings (an AU Intercom workspace needs `api_base`) |\n| `integrations.google.site_verification`, `<meta>` tags in head HTML | `seo.metatags` | other head HTML: report |\n| `stylesheet_hub2` (custom CSS) | a root `theme.css` | rules for ReadMe's classes (`.rm-*`, `.callout_*`, hashed names) match nothing: carry only rules for the site's own markup |\n| Footer HTML | `footer: { copyright, links }` | a copyright line → `copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`). Report the rest |\n| `variables.defaults` | `variables` | Content. **Defaults can be real credentials** (an API key): never copy one into config |\n| `glossaryTerms` | `--glossary hub.html` | the codemod writes `<Tooltip>`s |\n| `redirects` | `redirects` | URLs and redirects |\n| `error404` | `pages/404.astro` or Blume's default | URLs and redirects |\n| Ask AI, MCP server | `ai.assistant`, `agents.mcp` | need a server-output host adapter: report |\n\nThen the layout. **Leave the section folders where they are.** Set `content.root: \".\"` and scope `include` (`references/monorepo.md` §1), import the codemod's redirects, and add the reference:\n\n```ts\nimport { defineConfig } from \"blume\";\nimport { openapi } from \"blume/reference\";\nimport redirects from \"./readme-redirects.json\";\n\nexport default defineConfig({\n  content: {\n    root: \".\",\n    include: [\n      \"index.mdx\",\n      \"docs/**/*.mdx\",\n      \"reference/**/*.mdx\",\n      \"recipes/**/*.mdx\",\n      \"page/**/*.mdx\",\n      \"changelog/**/*.mdx\",\n    ],\n  },\n  redirects: [...redirects /* , your own */],\n  reference: [openapi({ sources: [/* API reference */] })],\n});\n```\n\n`page/` is ReadMe's custom pages at `/page/<slug>`; Blume's own `pages/` folder is for `.astro` pages, a different thing.\n\n## Navigation (shape A)\n\nWhat the codemod does, so you can check it:\n\n- **Categories → group folders.** `docs/Getting Started/` → `docs/(Getting Started)/`: the group keeps the label and adds no URL segment.\n- **`_order.yaml` → `meta.ts` `pages`**, entries as written (`pages` ignores the parentheses), less duplicates and entries with no file. A category folder's `meta.ts` also sets `title` where Blume would relabel the folder name (`Self-Serve` would show as \"Self Serve\"). A parent page's folder gets `title` from the parent and `display: \"group\"`.\n- **`slug` pins.** ReadMe serves every child flat (`/docs/<child>`; the nested path 404s), so every page below a parent folder gets `slug: docs/<stem>`, and a nested parent's own `index.mdx` takes `docs/<folder>`. A parent folder directly in a category needs none: its folder name is the route. Any stem starting with a digit gets one too, since Blume strips `^\\d+[-_.]` as an ordering prefix (`1-setup` → `/docs/setup`); dates (`12-05-2022`, `2024-01`, `2022-05-12`) and versions (`1.2`) stay whole, so their pins are harmless.\n- **Hidden pages** (`hidden: true`) were left out of the sidebar and search but served by link: `hidden: true` + `search.exclude: true`, URL kept.\n- **Empty parent pages** (frontmatter, no body) redirected to their first child on ReadMe. The codemod deletes them and adds a 302 to the first child; prefer that to keeping one as a `directory: \"card\"` landing page. A parent that stays (with a body, or kept as a landing page) whose child has the same title fails `validate --strict` (`BLUME_NAV_DUPLICATE_LABEL`): retitle one, and report it.\n- **Link pages** (`link: { url }`): deleted, with a 302 to the URL. If the link must stay visible, add it to `navigation.featured`.\n- **Section roots.** `/docs` and `/reference` redirect to the section's first page, as on ReadMe; a typed URL would 404 without it. ReadMe answers them with a 301; the codemod writes a 302, since the first page can change.\n- **Custom pages** had no sidebar: `hidden: true` plus `mode: center` (`fullscreen: true` → `mode: custom`).\n\nLeft for you: an HTML custom page (`custom_pages/<slug>.html`) → `page/<slug>.mdx` with its HTML as JSX, or `pages/page/<slug>.astro` on `PageLayout` for a full document with scripts (`docs/advanced/custom-pages.mdx`); and `/` (see `first_page`).\n\n### Versions\n\nVersion branches hold full copies of guides, recipes, and the reference; ReadMe serves the default version without a prefix and others at `/<version>/docs/<slug>`. **Recommend migrating the default version only**, with `{ from: \"/<version>/:path*\", to: \"/:path*\" }` for each other version and the default's own prefix (`/v3.0/docs/x` redirected to `/docs/x` on ReadMe). Pages that only exist in an old version then 404: report it. To keep old versions, run the codemod on each branch's checkout and copy its `docs/`, `recipes/`, and `reference/` into `<version>/` (`v2.1/docs/…` serves `/v2.1/docs/…`), drop that version's prefix redirect (a pattern over pages fails the build), add `\"<version>/**/*.mdx\"` to `content.include` (a scoped `include` never scans the copy otherwise), and set `versions.archived` (newest first) and `versions.current`. Each id is the URL segment (`v2.1`): ids must start with a letter. References, the changelog, and custom pages stay unversioned, and an archived tree's sidebar isn't scoped by tabs: report both.\n\n## Navigation (shapes B and C)\n\nRebuild the tree from frontmatter into shape A's layout, then run the codemod:\n\n- **Category** → `docs/(<Category title>)/`. Shape B's `category.uri` ends in the title (`/branches/3.0/categories/guides/Getting%20Started`): decode it. Shape C's `category` is an ID: resolve it with `GET /api/v1/categories`, the hub's sidebar, or the user. `categorySlug` names it.\n- **Parent** (`parent.uri`, `parentDoc`, `parentDocSlug`) → a folder named for the parent's slug, holding the parent as `index.mdx`; pin each child's `slug` as in shape A.\n- **Order** (`position`, `order`) → the folder's `meta.ts` `pages`.\n- **Slug**: frontmatter `slug`, else the file name. Name each file after its slug, and pin any that starts with a digit.\n- **Custom pages and changelog entries** → `page/<slug>.md` and `changelog/<slug>.md`, whatever folder CI uploaded them from: the codemod maps their frontmatter by those folder names (`custom_pages/` and `changelogs/` work too), but outside shape A it moves nothing, so the folder is the URL.\n\nThe codemod reports these navigation keys and leaves them: delete them once the tree is built.\n\n## Content\n\n**Every page becomes `.mdx`**: ReadMe's `.md` files are MDX, and in a Blume `.md` page directives stay literal text and components don't render. The codemod converts the mechanical syntax (table below). It also clears what ReadMe's lenient MDXish engine (`\"mdxish\": true` in the settings JSON) accepts and strict MDX rejects: it deletes `<!-- -->` comments, self-closes void tags, and converts `[block:…]` JSON. **It leaves everything that needs judgment, and reports it per file:**\n\n- **Inline components.** A page that defines a component (`export const Name = …`) is left byte for byte. Without hooks it renders as static HTML. With hooks it fails the build: move it to `islands/<Name>.tsx` (`export default`). One defined on several pages goes in `islands/` once.\n- **Custom components** (`custom_blocks/<Name>.mdx`): use a Blume component when it's presentational (a card grid → `CardGroup`), else move it to `islands/<Name>.tsx` and keep `<Name />` in the pages. Blume compiles Tailwind v4 utilities from the project's `.mdx` and `.tsx` files (Tailwind's default theme plus Blume's tokens, `dark:` keyed to Blume's theme), so utility classes keep working; ReadMe's CSS variables (`var(--color-border-default)`) don't exist in Blume: swap them for Blume's (`var(--blume-border)`) or its token utilities (`border-border`, `bg-muted`). Text moved from a prop into children needs `{` and `}` escaped (`\\{`), or the build fails (`BLUME_MDX_UNDEFINED_NAME`, at the brace's line).\n- **Event handlers.** `<button onClick={…}>`, `onMouseEnter`, and friends on inline HTML build green and do nothing; `blume check` and `build` warn `BLUME_MDX_EVENT_HANDLER` at each one. Rebuild the widget as an island (`grep -rnE 'on[A-Z][a-zA-Z]*=' --include='*.mdx'` outside code finds them before a build). Hubs often paste the same widget (a \"contact support\" box) into dozens of pages: one island, and replace every copy. A handler that calls a support widget's global (`Intercom('show')`) also needs that widget loaded (Config, `integrations.intercom`).\n- **`<HTMLBlock>` with `<style>`, `<script>`, or handlers** stays wrapped. **Don't move its `<style>` rules into `theme.css`**: ReadMe authors write global selectors (`p`, `ul`, `h3, h4`), which there restyle the whole site, sidebar included. Rebuild the markup with Blume components (`Tabs`, `Columns`, `CardGroup`, `Panel`, callouts). ReadMe stripped `<script>` unless `runScripts` was on: rebuild what a script did as an island or a `script()` adapter.\n- **Font Awesome `<i>` icons** in raw HTML (`<i className=\"fa fa-plug\" />`): `<Icon icon=\"<lucide>\" />`; Blume loads no Font Awesome, so the `<i>` renders empty. Tailwind classes in raw HTML (`className=\"mt-4 p-4 bg-gray-50\"`) still compile, but fixed colors ignore dark mode: prefer a component (`Panel`, a callout) or Blume's token utilities (`bg-muted`, `border-border`).\n- **Unknown components** (`<PostmanRunButton>`, `<MCPIntro>`, `<TableOfContents>`): drop or replace; each fails the build.\n- **Variables.** `<<name>>`, `{user.name}`, and `<Variable name>` became `{{name}}`; define each in `variables` (from `variables.defaults`). ReadMe filled them per reader; Blume shows one default to everyone (report). The codemod leaves `<<name>>` inside code as written and reports it: change one to `{{name}}` by hand where it should show the default (Blume substitutes in code too).\n- **Glossary terms** with no definition: pass `--glossary`, or write the `<Tooltip>` by hand.\n- **Dead links.** Hubs collect links to slugs from older versions that 404 on the live site too. Check a few live first (ReadMe cross-redirects some, `/reference/x` → `/docs/x`), then point each at the closest current page, or unlink it and keep the text. Report the list.\n- **Images** stay on `files.readme.io`: download them into `public/` and rewrite the URLs (`grep -rhoE 'https://files\\.readme\\.io/[^)\" ]+'`). Some have no extension: add one. Review every alt the codemod derived from a file name.\n- **Tables** with lists or blocks in their cells, or spanning cells (`colSpan`, `rowSpan`), stay `<table>`: check they render.\n- **Recipes**: the codemod deletes the line-highlight comments and demotes the step headings; the walkthrough modal is dropped (report). Repeat a highlighted excerpt under its step with `{1-4}` where it mattered.\n\n| ReadMe | Blume (codemod) |\n| --- | --- |\n| `> 📘 Title` / `>` / `> Body` (emoji first) | `:::info[Title]` … `:::`; 📘 ℹ️ → `info`, 👍 ✅ → `success`, 🚧 ⚠️ → `warning`, ❗️ 🛑 ‼️ ⁉️ → `danger`, any other emoji → `note`. An emoji alone means no title; `#` marks drop |\n| `<Callout icon theme>` | the directive for `theme` (`okay` → success, `warn` → warning, `error` → danger, `default` → note), else for `icon`; a first-child heading becomes the title |\n| `[block:callout]`, `[block:code]`, `[block:image]`, `[block:table]`/`parameters`, `[block:api-header]`, `[block:embed]`, `[block:html]` | the same conversions as their component forms |\n| adjacent fences (ReadMe's code tabs) | `<CodeGroup>`; untitled tabs labeled with the language, as ReadMe showed them |\n| ` ```javascript Node SDK ` | ` ```javascript title=\"Node SDK\" `; `node` → `js`, `curl` → `bash`, `cplusplus` → `cpp`, `objectivec` → `objc` |\n| `<Image src alt caption>`, `<Image …>caption</Image>`, `![1544](url \"image.png\")` | `![alt](src)`, in `<Frame caption>` when captioned; numeric or missing alts derived from the file name; `align`/`width`/`border` drop |\n| `<Embed>` (YouTube, including `google.com/sorry` interstitials), `[x](url \"@embed\")` | `<YouTube id>`; anything else → a link |\n| `<HTMLBlock>{`…`}</HTMLBlock>` | its HTML, inline: comments removed, void tags closed, braces as entities |\n| `<Table>` | a Markdown table: a cell's paragraphs joined with `<br />`, all-empty rows dropped |\n| `<Cards columns>` / `<Card iconColor badge target>` | `<CardGroup cols>` / `<Card color>`, `badge` → `<Badge>`, `target` drops; braces from props escaped |\n| `<Columns layout>`, `<Tab icon>` | `<Columns cols>`, Lucide icons |\n| `<Accordion title>` | a run → one `<Accordion>` of `<AccordionItem>`s; one alone → `<Expandable>`, or a one-item `<Accordion>` when it has an icon (`Expandable` takes none) |\n| `<Glossary>`, `<<glossary:x>>` | `<Tooltip tip>` |\n| `<Anchor>`, `<Recipe>`, `[block:recipe]`, `[block:tutorial-tile]` | a link, `<Card href=\"/recipes/<slug>\">` |\n| `custom_blocks/<Name>.md` used as `<Name />` | `_snippets/<name>.mdx` + `<include>/_snippets/<name>.mdx</include>` |\n| `doc:x`, `ref:x`, `page:x`, `changelog:x`, `blog:x`; `https://<site>/docs/x` | `/docs/x`, `/reference/x`, …; root-relative |\n| Font Awesome (`fad fa-hand-wave`) on frontmatter, cards, tabs, accordions | Lucide, or dropped and reported |\n| `:tada:` | 🎉 (unknown names reported) |\n| a body `# H1` repeating the title; any other body H1 | deleted; every body heading demoted one level |\n\nCallout titles had anchor ids on ReadMe; Blume's don't. Headings otherwise keep their anchors: both slug with github-slugger, so skip `pin-heading-ids.mjs`, which would also crawl the rate-limited hub. One exception: ReadMe's slugger also counted callout titles and the H1 the codemod deletes, so a later heading with the same text was `<slug>-1` on ReadMe and is `<slug>` in Blume. Another: for a heading that ends in an emoji or symbol after a space, github-slugger keeps a trailing dash (`## Features ✨` → `#features-`), which Blume drops (`#features`). `blume validate --strict` flags links to the old id; retarget them.\n\n## Frontmatter\n\nBlume's page schema is strict. The codemod maps:\n\n| ReadMe | Blume |\n| --- | --- |\n| `title` | `title` |\n| `excerpt` (A, C), `content.excerpt` (B) | `description` |\n| `hidden: true` (A), `privacy.view: anyone_with_link` (B) | `hidden: true` + `search.exclude: true` |\n| `hidden: true` (C) | the same, reported: rdme maps it to `privacy.view: anyone_with_link`, but check the live URL and use `draft: true` if it 404s |\n| `deprecated`, `state: deprecated` (B) | `deprecated: true` |\n| `icon`, `appearance.icon` (B) | `icon` (Lucide) |\n| `metadata.title` / `description` / `image` | `seo.title` / `description` / `image`; B's `image: { uri }` is an image id, not a URL: reported |\n| `metadata.keywords` | `search.keywords` |\n| `metadata.robots: noindex`, `allow_crawlers: disabled` (B) | `noindex: true` |\n| `next.pages`, `content.next` (B) | `related`, dropping entries that aren't pages in the repo; Blume takes 10 at most (the rest reported) |\n| `link`, `content.link` (B) | a redirect (Navigation) |\n| `table_of_contents: false` | `mode: wide` (an approximation: report) |\n| `fullscreen` (custom pages) | `mode: custom`, else `mode: center` |\n| `recipe`, `x-import`, `name`, `api`, `api_config`, `recipes` | dropped (`recipes` reported) |\n\nIt keeps keys it doesn't know and reports them: Blume rejects unknown keys, so map or delete each. It drops C's `type` (`basic`, `link`, `error`), since Blume's own `type` sets the content type. Changelog keys are under Changelog.\n\n## API reference\n\nEvery endpoint page is generated from an OpenAPI file. The codemod records each one's old URL, operation, and prose in `readme-migration.json`, and deletes it.\n\n1. **Specs → sources.** One `openapi()` source per spec in `reference/`, labeled with the ReadMe category its endpoints sat under, or the spec's `info.title`: `{ label: \"Connect\", spec: \"./reference/connect.json\" }` serves `/reference/connect`, with operations at `/reference/connect/<tag>/<operation>`. **A category that held several specs** (the report lists them) can't share a label: they'd share a route and the first would win. Give each its own label, or merge the specs first, and report the split. A single spec → `openapi({ spec })`. Add a `navigation.tabs` entry for `/reference`. A YAML spec: convert it to JSON (`npx @redocly/cli bundle x.yaml --output x.json`) and rerun the codemod, which reads JSON only.\n2. **Overlays.** `reference/overlays/<spec>.overlay.json` sets each operation's `description` to its spec text plus the prose its ReadMe endpoint page showed, and converts the ReadMe Markdown inside spec descriptions (emoji callouts, shortcodes, `doc:` links), which ReadMe rendered and Blume would show plain. Add each to its source: `{ label, spec, overlays: [\"./reference/overlays/connect.overlay.json\"] }`. JSX doesn't render in descriptions (`<` and `{` are escaped): rewrite any the report names as Markdown.\n3. **Build, then run the second pass.** List the generated routes, then hand them to the codemod with the same `--spec` pairs (the source's route, then its spec):\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist --spec /reference/connect=reference/connect.json > routes.json\n   node <skill>/scripts/readme-codemod.mjs --routes routes.json --spec /reference/connect=reference/connect.json --write .\n   ```\n\n   It adds a 301 from each old endpoint URL (`/reference/getaccount`) to its operation page, and a 302 from each tag page (`/reference/accounts-1`) to the tag's first endpoint, as ReadMe answered. It skips an old URL that is now a page (`/reference/enrich`, ReadMe's operation, against an Enrich source's overview): a redirect there would hide the page (`BLUME_REDIRECT_MATCHES_PAGE`). It reports each, so you can pick another `route` instead. It adds a 302 from `/reference` to the first endpoint when nothing else claims it, and drops any pass-one redirect that would now hide a reference page. It also rewrites links to old endpoint and tag URLs outside fenced code, dropping anchors (reported), and writes `<route>/<tag-slug>/meta.ts` (`defineMeta({ title, order, pages })`) with ReadMe's endpoint order, where it can differ from the spec's order, which the generated sidebar follows.\n\n4. **Source order, by hand.** Write `<route>/meta.ts` → `defineMeta({ order: <n> })` for each source, with the order the report gives (ReadMe's category order). It merges over the generated group's settings, so the source's `label` stays its title.\n\n- **Conceptual pages** (no `api` key) stay in `reference/(<Category>)/` at `/reference/<slug>` and join the reference tab's sidebar; their group folder's `meta.ts` takes an `order` among the sources.\n- **`reference/ReadMeConfig/`** (`api_config` pages: getting started, authentication, my requests) showed the reader's own API key and request log. The codemod keeps any prose as a page and deletes empty ones: redirect each deleted URL to the closest page.\n- **Hidden endpoints**: remove the operation with an overlay action (`remove: true`) if it should stay unpublished. `x-internal: true` likewise: Blume ignores the flag.\n- **`x-readme` extensions** (and flat spellings like `x-samples-languages`, at the root or on an operation): `samples-languages` → `codeSamples`: `shell`, `node`, `javascript`, `python`, `ruby`, `go`, `php`, `java`, `csharp`, `kotlin`, `swift`, `c`, `cplusplus` (read as `cpp`), and `powershell` carry over as written (so does `curl`, which some specs use); `objectivec`, `http`, `json`, `clojure`, `ocaml`, and `r` have no Blume generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: leave them out and report them. `code-samples` → `x-codeSamples` by overlay (`language` → `lang`, `code` → `source`, `name` → `label`). `explorer-enabled: false` → `playground: false`; `proxy-enabled` (on by default: Try It went through ReadMe's CORS proxy) → `playground: { proxy: true }` on a server-output host, or the API must allow the docs origin (say which). `codeSamples` and `playground` are per `openapi()` adapter, not per source or operation: when specs or operations disagree, take the setting most of them use and report the rest. `samples-enabled` is no longer supported on ReadMe (it hid nothing): ignore it. `headers`, `x-default`, `oauth-options`, `parameter-ordering`, `metrics-enabled`: no equivalent (report).\n- **Versioned references**: unversioned in Blume; an archived version's spec can be its own source with `route: \"/<version>/reference\"` (report).\n\n## Changelog\n\nThe codemod moves `changelogs/<slug>.md` → `changelog/<slug>.mdx` (same URL; digit-led stems pinned with `slug`, which date names no longer need) and adds `type: changelog`:\n\n| ReadMe | Blume |\n| --- | --- |\n| `date`, `published_at` (what synced repos contain), `created_at` (B) | `date` (the day). None (common in C) → reported: take it from the live entry |\n| `author` | `authors`; B's ID object drops |\n| `type` (`added`, `fixed`, `improved`, `deprecated`, `removed`; `none`) | `changelog.category`, capitalized; `none` omitted |\n| `hidden: true`, `privacy.view: anyone_with_link` | `hidden: true`: still built, out of the index and feed |\n\nIt also adds `/changelog.rss` → `/changelog/rss.xml`; the feed needs `deployment.site` or a detected host. Add a `{ label, path: \"/changelog\" }` tab. If the project is open source on GitHub and its releases are the product changelog, offer `githubReleases()` (SKILL.md \"Changelogs\").\n\n## URLs and redirects\n\n**The old URL list**: `readme-migration.json` lists every URL the repo served (hidden pages included) with its kind; add `sitemap.xml` (it omits hidden pages and empty parents), `llms.txt`, other versions (`/<version>/docs/…`), and Enterprise or translated segments (`/<project>/`, `/lang-de/`). At the end every entry must be a page or a redirect to one.\n\n- **Status codes.** Use 302 where ReadMe answered with a 302 (empty parents, tag pages, `/`), for link pages, and for section roots (ReadMe's 301 points at whichever page is first, which can change), and the default 301 for moves (operations). The codemod does, except `/`, which it leaves to you (`first_page`).\n- **ReadMe's own redirects** (`redirects` in the settings JSON, or Settings → Errors & Redirects: `old -> new` per line, full-match regex with `$1`, applied only when no page matches): a literal rule → `{ from, to }`; one-segment or rest-of-path patterns → `:name` / `:name*` (`/old(/\\S*)? -> /new$1` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`, which also sends `/old` to `/new`); anything else → host rules (report).\n- **Versions**: `{ from: \"/<version>/:path*\", to: \"/:path*\" }` (Versions).\n- **The custom 404** (`error404`): rebuild that custom page as `pages/404.astro`, or keep Blume's default. Keep or redirect its `/page/<slug>` URL.\n- **`blume validate`** accepts a redirect `from` as a link target, so rewrite links before trusting it. It warns about a redirect whose `to` leads nowhere (`BLUME_BROKEN_REDIRECT`), but not about loops or chains: run `blume audit --only redirects` after each build. A link to `/llms.txt` or another file Blume generates passes while the config has that feature on.\n- **Check every old URL** against `dist/` and `dist/blume-redirects.json` (no host adapter), following redirects until each lands on a page, or request each from `blume dev` or `blume preview` with `curl -sL`: both answer every redirect, patterns included.\n\n## Teardown\n\n**Disconnect the sync first.** With bi-directional sync on, ReadMe's GitHub app reads and writes every branch named after a version, so restructuring one rewrites the live hub. Migrate on a branch that matches no version (`blume-migration`; `<version>_<name>` would sync), and have the user disconnect the repository (Settings → Git Connection) before merging. Remove every `rdme … upload` step from CI in any shape (and an `rdme` dependency); `rdme openapi validate` can stay as a spec linter.\n\nDelete `custom_blocks/` once converted, `reference/ReadMeConfig/` leftovers, `frontmatter.schema.json`, `hub.html`, `routes.json`, and `readme-migration.json` once the redirects are final (keep `readme-redirects.json`: the config imports it). Scaffold a `package.json` with `blume` and `dev`/`build`/`preview` scripts. Moving the custom domain's CNAME is the user's step: list it.\n\n## Dropped — report these\n\n- **Hosted features**: the editor, Branches and Reviews, the AI Linter and Agent, suggested edits (Blume's edit link comes from `github`), discussions and the dev FAQ, API Metrics, My Developers, My Requests and API logs, the Developer Dashboard, health status updates, and the landing page builder (rebuild as `index.mdx` with `mode: custom`, or `pages/index.astro`).\n- **Personalized docs**: per-reader variables, API keys in Try It, `x-default` credentials, and the `ReadMeConfig` widgets. Blume's Try It takes credentials the reader types.\n- **Login-gated docs** (custom login, JWT, site password, hidden versions): Blume has no sign-in. Point at host-level protection (deployment docs, **Private docs**); it covers the whole site, so public and private pages need two sites.\n- **Changelog options** (layouts, relative dates, author toggle, translations), recipe walkthrough modals, Global Reusable Content across projects, translations through Localize or Transifex (offer `i18n` and `blume translate`), and Enterprise cross-project navigation.\n- **Approximations**: `mode: wide` for `table_of_contents: false`, image sizes and alignment, emoji and unmapped icons, unmapped `x-readme` extensions, unversioned references and changelog, custom components restyled as islands, and HTML or scripts dropped from custom blocks.\n",".agents/skills/blume-migrate/references/redocly.md":"# Redocly → Blume\n\nCovers **Redocly Realm** and the products on the same project format (Redoc, Revel, Reef, alone or combined), usually hosted and edited in **Reunite**. A project is a `redocly.yaml`, `.md` pages written in **Markdoc** (`{% tag %}`, not MDX), optional `sidebars.yaml` files, and API description files in the content tree, each rendered as a reference **at its own path**. The work: Markdoc tags become directives and components, each page's body H1 becomes `title`, sidebars become folders and tabs, and every API operation URL changes shape, so the old ones get generated redirects.\n\n## Detect\n\nA **Realm-family project** has at least one of these (`redocly.yaml` is optional; Realm builds a bare folder of Markdown):\n\n- a `sidebars.yaml`, or a prefixed `<name>.sidebars.yaml`, at the root or in a section folder;\n- `.md` pages using Markdoc tags (`{% admonition`, `{% tabs`, `{% partial`);\n- `@theme/`, `_partials/`, `@l10n/`, or `@<version>/` folders, or `*.page.tsx` React pages;\n- a dependency on `@redocly/realm`, `@redocly/redoc`, `@redocly/revel`, `@redocly/reef`, or a combined package (`@redocly/redoc-revel`, `@redocly/redoc-reef`, `@redocly/revel-reef`). Reunite builds projects with no `package.json`, so a missing one proves nothing.\n\nThree look-alikes:\n\n- **A lint/bundle-only `redocly.yaml`**, with none of the above and only keys Redocly CLI reads (`extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `apis`, `resolve`, `telemetry`), configures `redocly lint`/`bundle`. Leave it untouched and migrate the framework the docs actually use.\n- **The legacy Redocly Developer Portal** (Gatsby; `siteConfig.yaml`, `theme.ts`, a `@redocly/developer-portal` dependency, `.mdx` pages, `*.page.yaml` reference pages, and a `sidebars.yaml` that maps named sidebars to groups of `pages:`). Its content is MDX with `:::` admonitions, so this file's Markdoc mappings don't apply: use SKILL.md's general model, this file's Config, API reference, and URL sections, and tell the user it was the legacy portal.\n- **A standalone Redoc embed** (an HTML page loading `redoc.standalone.js` with `<redoc spec-url>` or `Redoc.init`, or a `redocly build-docs` script): migrate only the spec, with `openapi({ spec })`, and rewrite links to its `#tag/…`/`#operation/…` anchors. A Redoc-product project (`@redocly/redoc` alone) has no Markdown either.\n\n**Keep the content where it is**: `content.root: \".\"`, with `content.include` scoped to the real content folders (`[\"guides/**/*.{md,mdx}\", \"apis/**/*.{md,mdx}\"]`, per `references/monorepo.md` §1), plus each root-level page by name (`\"index.{md,mdx}\"`, `\"changelog.{md,mdx}\"`). A root `*.md` glob would also publish `README.md`, which Realm never did. Routes then keep their paths, `/`-rooted partial paths keep resolving, and the description files stay where `redocly.yaml`'s `apis.*.root` and the lint scripts expect them.\n\n**Realm publishes every `.md` in the project** except the root `README.md` and `public/`, partials folders, `static/`, `@theme/`, and the `ignore` globs, so the old sitemap often lists accidental pages: `.github/pull_request_template`, files under `.claude/`, planning notes. Leave those out of `content.include`, add no redirect for them, and list them in the report so the user can add one if they want full URL coverage. Underscore-prefixed files outside a partials folder were pages in Realm but are excluded by Blume's default `**/_*`: rename any that are real pages (`content.exclude: [\"!**/_*\"]` would publish every partial too).\n\n## Config: `redocly.yaml` → `blume.config.ts`\n\nResolve `$ref`s first (`redirects: { $ref: ./redirects.yaml }` is common). Product folders can hold their own `redocly.yaml`, and `env.<environment>` blocks override per environment: map the production values and report the rest.\n\n| Redocly | Blume | Notes |\n| --- | --- | --- |\n| `seo.title` (`seo.projectTitle` when set) | `title` | Blume appends ` - <title>` to every docs page's `<title>` (not to a page titled the same as the site). Realm added a suffix only with `projectTitle`, so on most Realm sites this is a visible change: report it |\n| `seo.description` | `description` |  |\n| `seo.siteUrl` | `deployment.site` | unless the new host is Vercel or Netlify (auto-detected). Cloudflare Pages exposes only a per-deploy URL, so keep it there |\n| `seo.meta` (`[{ name, content }]`), `seo.keywords` | `seo.metatags` (`{ [name]: content }`, `keywords: \"a, b\"`) | minus the tags Blume writes itself; the build names the setting that owns a refused tag |\n| `seo.lang` | nothing for English; else `i18n: { defaultLocale: \"<code>\", locales: [{ code: \"<code>\", label: \"<language name>\" }] }` | `label` is required |\n| `seo.image`, `seo.jsonLd`, `seo.llmstxt` | **drop** (report) | Blume generates per-page cards, JSON-LD, and `llms.txt`; a page in `llmstxt.excludeFiles` gets `ai.exclude: true` |\n| `logo.image` / `logo.srcSet` (`\"./l.svg light, ./d.svg dark\"`), `altText`, `link` | `logo` / `logo: { image: { light, dark, alt }, href }` | move the files into `public/`; a wordmark also gets `text: \"\"` |\n| `logo.favicon` | **drop the field**: copy the file to `favicon.<ext>` or `icon.<ext>` in the root or `public/` | detected by filename, for `.svg`, `.png`, and `.ico` only: convert any other format |\n| `navbar.items` | `navigation.tabs` / `actions` / `cta` | see Navigation |\n| `footer.items` (columns, each a `{ group, items }` or a single item) | `footer.links` (one row) + `footer.socials` | flatten every column's items in order (`page` → its route). Links to a platform in Blume's `footer.socials` list (`docs/configuration/index.mdx`) become socials; a podcast (Spotify, Apple Podcasts) is `podcast`; others (TikTok) stay links. `copyrightText` → `footer.copyright`. Column labels drop (report) |\n| `redirects` (`'/from': { to, type }`) | `redirects: [{ from, to, status: type }]` | write `from` without a trailing slash; a trailing `/*` carries over as written (`{ from: \"/a/*\", to: \"/b/*\" }`), and also matches the bare `/a`. Blume takes 301/302/307/308 only |\n| `markdown.toc` | `toc: { maxHeadingLevel: <depth> }`, or `toc: false` for `hide` | Realm's default depth 3 is Blume's default |\n| `markdown.lastUpdatedBlock` | `lastModified: \"git\"`, **only when declared** and not `hide: true` | Realm shows \"Last updated\" by default; if the source left it at that, offer `lastModified: \"git\"` in the report. Either way, every page the migration edits (nearly all of them, since each H1 moves) shows the migration commit's date; a rename alone keeps the page's date. Say so |\n| `markdown.editPage.baseUrl` | `github: { owner, repo, branch, dir? }` | the path after the branch → `dir`; a non-`github.com` origin → `host` |\n| `ignore` | `content.exclude: [ … ]` | the globs add to Blume's defaults (`**/_*`, `**/.*`), so `_partials/` stay out of routing |\n| `colorMode` | `theme.mode` | `ignoreDetection` with a first mode of `dark` → `\"dark\"` |\n| `banner` (a list) | `banner: { content, link: { href, text }, dismissible }` | one banner: keep the first unscheduled, untargeted one. `content` is plain text, so a Markdown link in it becomes `link`; colors, targets, schedules drop |\n| `feedback` | `hide: true` → `feedback: false`; `type: comment` → `{ comments: true }` | other types drop |\n| `search` | omit (built-in Orama); `suggestedPages` → `search: { provider: orama(), popular }` (`orama` from `blume/search`) | `engine` and `filters` (facets) drop |\n| `analytics` | `ga.trackingId` (a `G-` id) → `googleAnalytics({ id })`, `gtm.trackingId` → `googleTagManager({ id })`, `amplitude.apiKey` → `amplitude({ key })`, `segment.writeKey` → `segment({ key })`, `heap.appId` → `heap({ id })`, `adobe.scriptUrl` → `adobe({ url })`, `fullstory`/`rudderstack` → `script()` | from `blume/analytics`. A `UA-` id is Universal Analytics, dead since 2023: drop it and say so |\n| `scripts.head` / `scripts.body` | one `script({ src, strategy, attributes })` each in `analytics` | local `./static/x.js` → `/x.js`; `async`/`defer` → `strategy`. **Adapters in `analytics` load only in production builds and wait for consent when `consent` is set.** A script the page needs to work (a support widget, a form embed that buttons call) goes in a `PageFooter` layout slot component instead (`<script is:inline src=\"…\">`), which runs in `blume dev` too. That slot renders on every content page, API operation pages included, but not on `mode: custom` or `frame` pages or custom `pages/`: report where the script is now missing |\n| `links` | a Google Fonts stylesheet → `theme.fonts` (`{ body, display, mono }`) | each role a curated slug, or `{ name: \"Family\" }` for anything else; mind renamed families (Source Sans Pro is `source-sans-3`). `preconnect` links drop |\n| `access` (`requiresLogin`, `rbac`, `sso`, `idps`), root `rbac`/`requiresLogin`/`sso`, `ssoDirect`, `userMenu` | **drop** | Blume has no login: point at host-level protection (deployment docs, **Private docs**), which covers the whole site; report every protected path |\n| `l10n` (older: `i18n`) | `i18n` | see i18n |\n| `apis`, `openapi`, `graphql` | `reference` adapters; **keep `apis` in `redocly.yaml`** | see API reference |\n| `products` (`{ id: { name, folder, icon } }`) | `navigation.selectors: [{ kind: \"product\", label, items: [{ label: name, path: \"/<folder>\" }] }]` |  |\n| `aiAssistant`, `mcp` | nothing | offer `ai.assistant` / `agents.mcp` (server output) as follow-ups |\n| `mockServer`, `catalogClassic`, `entitiesCatalog`, `scorecard*`, `developerOnboarding`, `apiFunctions`, `skills`, `corsProxy`, `responseHeaders`, `reunite`, `removeAttribution`, `palette`, `breadcrumbs`, `codeSnippet`, `navigation`, `navbar.hide`, `footer.hide`, `sidebar.hide` | **drop** (report) | `consent` → a `blume/consent` adapter if the site runs one; `navigation.actions` are Blume's default page actions |\n| `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, `recheck` | **keep in `redocly.yaml`** | see Teardown |\n| `'{{ process.env.X }}'` | `process.env.X` in `blume.config.ts` |  |\n\n## Navigation: `sidebars.yaml` and `navbar` → folders and tabs\n\nWith no sidebar file, Realm generates the sidebar from the file tree (natural sort, `index` first), which Blume's generated sidebar reproduces. Otherwise every file ending in `sidebars.yaml` is a sidebar (one only pulled in through another's `$ref` isn't), `page:` paths resolve from the sidebar file (or the project root with a leading `/`), and **a page no sidebar lists shows no sidebar** (unless its frontmatter `sidebar.path` names one): in Blume's generated tree it would appear. `sidebar.hidden: true` hides it, but also drops it from search, the sitemap, and `llms.txt`, where Realm still listed it: hide only orphans nobody should find, and report which you hid. Realm labels default to the page's `seo.title`, then its first heading, so most `label`s only restate the new `title`.\n\n- **`group` + `items`** → a folder with a `meta.ts` (`title`, and `pages` in the declared order). A group whose pages sit flat beside others, or come from several folders, has no on-disk counterpart: move its pages into a subfolder (a page the group opened becomes its `index`, keeping its URL; the others get redirects) or use an explicit `navigation.sidebar`, per SKILL.md \"Config-declared nesting\".\n- **`label`** → `sidebar.label`, only where it differs from `title`. **`href`** → an explicit sidebar link or `navigation.featured`. **`directory`** → leave the folder to the generated sidebar. **`$ref`** → inline it.\n- **`expanded`**: Realm groups collapse by default, which `navigation.sidebar.display: \"group\"` reproduces. `expanded: true`/`always` → `collapsed: false` in that folder's `meta.ts`. **`menuStyle: drilldown`** → `display: \"page\"`.\n- **`separator`** and **`separatorLine`** have no equivalent: a labeled separator heading a run of pages is usually better as a group; report the rest.\n- **`badges`** → `sidebar.badge` (first name). **`icon`** → Lucide (Icons). `rbac`, `disconnect`, `additionalProps`, `external`, `*TranslationKey` drop.\n- **An API description listed as a `page`** places that API's generated tree. Blume can't put generated operation pages into an explicit `navigation.sidebar`; they sit wherever the source's `route` puts them (see API reference).\n\n**Several sidebars → header tabs.** Realm gives each page the sidebar that lists it, so one sidebar file per section (`guides/sidebars.yaml`), or top-level `tab:` entries in `sidebars.yaml`, is a tabbed site: one `navigation.tabs` entry per section folder (a `tab` with `menu:` → a tab with `items`). The root sidebar's pages, if they share no folder, get a tab with `path: \"/\"` and an `href` to their landing page; Blume picks the longest matching tab path, so section routes still select their own tab.\n\n**`navbar.items`**: an item that opens a section with its own sidebar is that section's tab. The first item into the root sidebar is the root tab; other items into the same sidebar become `navigation.actions`, or drop if they duplicate a tab's target. A \"Sign up\" button → `navigation.cta` with an absolute URL; a GitHub link → `github`.\n\n## Content: Markdoc → MDX\n\nBlume parses `.md` as plain Markdown; directives, components, and ` ```mermaid ` fences need `.mdx`. **Rename only the pages that end up with one of those** (`<include>` works in `.md` too), and leave the rest `.md`, so the diff stays small and their history keeps its file names. Then fix what MDX parses differently, in pages and in every partial an `.mdx` page includes (an included partial parses as MDX):\n\n- **Angle-bracket autolinks** (`<https://…>`, `<team@acme.com>`) fail to compile: write `[text](url)` or a bare URL.\n- A literal `{` or `}` in prose: escape it or put it in inline code. **HTML comments** → `{/* */}`.\n- **Raw HTML** must be JSX-valid: string `style=\"…\"` → `style={{ … }}` or a class in `theme.css`; close void tags (`<br />`). `<figure>` + image + `<figcaption>` reads better as `<Frame caption=\"…\">` around the image. A raw `<iframe>` of a YouTube video → `<YouTube id=\"…\" />`.\n- **File links follow the rename**: Blume resolves `./setup.md` by source file, trying the other extension when that file is gone, so a link to a page renamed `setup.mdx` still lands on it, `slug` and numeric prefix included. Update them anyway: a link to the old name breaks wherever the Markdown is read as files, such as on GitHub.\n\n**Every Markdoc tag must be converted**: in `.md` an unconverted tag ships as literal text with a green build, and `blume check` and `build` only warn `BLUME_TEMPLATE_TAG` at its line (in `.mdx` the page fails to compile, and `blume check` and `validate` report it as `BLUME_MDX_SYNTAX`). Finish with `grep -rn '{%' <content folders>` returning only samples that document Markdoc itself, in code where the warning skips them.\n\n**The body H1 is the title.** Realm renders the page's first `# H1` and uses it for `<title>`, sidebar label, and breadcrumb (unless the page sets `seo.title`; a page with no H1 uses its first heading of any level). Move it to frontmatter `title`, delete it from the body, and demote any later H1 to `##`. Links to the H1's own anchor (`#<h1-slug>`, often the first entry of a hand-written contents list) now break: remove them or point them at the page.\n\n| Markdoc | Blume (`.mdx`) |\n| --- | --- |\n| `{% admonition type=\"info\" name=\"Title\" %}` … `{% /admonition %}` | `:::info[Title]` … `:::` (`success`, `warning`, `danger` keep their names; `idea` → `tip`). No `name` → no brackets |\n| `{% tabs %}` / `{% tab label=\"X\" %}` | `<Tabs>` / `<Tab title=\"X\">`; `tabs id=\"k\"` → `param=\"k\"`. **A group of tabs that each hold only a code fence** → `<CodeGroup>` with titled fences (` ```ts title=\"X\" `); a bare fence directly inside `tabs` was a tab named by its `label` |\n| `{% code-group %}` | `<CodeGroup>` with titled fences; `mode=\"dropdown\"` → `<CodeGroup dropdown>` |\n| `{% partial file=\"…\" variables={a: \"x\"} /%}` | `<include a=\"x\">…</include>` on its own line (Partials) |\n| `{% raw-partial file=\"…\" /%}` | inline the file's text |\n| `{% code-snippet file=\"./x.js\" title=\"x.js\" /%}` | `<include lang=\"js\" meta='title=\"x.js\"'>./x.js</include>` (the file inside `content.root`). `from`/`to`/`before`/`after` ranges: paste the lines into a fence instead (report) |\n| `{% cards columns=N %}` / `{% card title to icon image %}` | `<CardGroup cols={N}>` / `<Card title href icon img>`; `layout=\"horizontal\"` → `horizontal` |\n| `{% accordion-group %}` / `{% accordion title expanded %}` | `<Accordion>` / `<AccordionItem title defaultOpen>` |\n| `{% numbered-list %}` / `{% numbered-item %}` | `<Steps>` / `<Step title>` (an item's leading heading becomes `title`) |\n| `{% img src alt caption /%}` | `![alt](src)`, inside `<Frame caption=\"…\">` when it had a caption. `srcSet` light/dark pairs have no equivalent: keep one image (report) |\n| ` ```mermaid ` fence, `{% diagram type=\"mermaid\" %}` | a ` ```mermaid ` fence (inline the file), in an `.mdx` page. PlantUML and Excalidraw have no renderer: export to SVG (report) |\n| `{% icon name=\"…\" /%}`, `{% inline-svg file /%}` | `<Icon icon=\"<lucide-name>\" />`, `![alt](./file.svg)` |\n| `{% table %}` | a pipe table (raw HTML for `colspan`/`rowspan`) |\n| `{% if %}` / `{% else /%}` | no conditionals: keep the branch the live site showed; per-reader branches (`$rbac`, `$user`) keep the public one (report) |\n| `{% $frontmatter.x %}` / `{% $env.PUBLIC_X %}` | the literal value / `{{public-x}}` with a `variables` entry (`\"public-x\": process.env.PUBLIC_X ?? \"\"`: values must be strings, so an unset variable would fail the config) |\n| `{% json-schema %}`, `{% json-example %}`, `{% openapi-code-sample %}`, `{% openapi-response-sample %}`, `{% replay-openapi %}` | a fenced sample or a `TypeTable`, or a link to the operation page, which renders samples and Try it itself (report) |\n| `{% markdoc-example %}`, `{% login-button %}`, `{% connect-mcp %}`, `{% code-walkthrough %}` | the fence only / drop (report); a walkthrough becomes prose and fences or `<Steps>` |\n| `## Heading {% #id %}` / `{% .class %}` | `## Heading [#id]` / drop |\n| any other tag | a custom tag from `@theme/markdoc/schema.ts`: rewrite its markup, or make its component an island (`islands/<Name>.tsx`, used as `<Name …/>`) |\n\n**Code fences**: Realm reads fence meta only from an annotation: ` ```js {% title=\"app.js\" highlight=\"{2,4-6}\" %} ` → ` ```js title=\"app.js\" {2,4-6} `, and `highlight=\"/word/\"` → `// [!code word:word]`. A bare word after the language was ignored by Realm but becomes a title in Blume: delete stray ones. `[!code highlight|++|--|word|focus|error|warning]` comments carry over.\n\n**Partials** live in `_partials/` folders at any depth, which Blume also keeps out of routing. `markdown.partialsFolders` replaces that default: add the folders it names to `content.exclude` (which keeps Blume's defaults) or rename them with a leading `_`. A leading `/` in `file` meant the project root, which is `content.root` when that's `\".\"`. Variables become lowercase attributes (`favFood` → `favfood`), read in the partial as `{{favfood}}`; a partial that read `$frontmatter` needs the value passed in. A partial used only by `.md` pages can stay `.md`.\n\n## Frontmatter\n\n| Redocly | Blume |\n| --- | --- |\n| body `# H1` | `title` |\n| `title` (top level) | **drop.** Realm never rendered it on Markdown pages (`<title>` was the H1), so moving it to `seo.title` would change live titles. Offer it in the report when it looks like a deliberate SEO title |\n| `description` (top level) | `description` (Realm used the site description instead; this improves the meta description) |\n| `seo.title`, `seo.description`, `seo.image` | the same keys (`seo.title` was also Realm's sidebar label: copy it to `sidebar.label` to keep that) |\n| `seo.keywords` | `search.keywords`, as a list (a string splits on commas). Blume uses them as search terms and writes no per-page `<meta name=\"keywords\">` |\n| `excludeFromSearch: true` | `search.exclude: true` + `ai.exclude: true` (+ `noindex: true` to keep it out of the sitemap, as Realm did) |\n| `slug` | `slug` (a full route); a list → the first as `slug`, the others as redirects |\n| `redirects: { '/old/': {} }` | a config `redirects` entry per path, `from` without the trailing slash. Exact entries beat wildcards, as in Realm |\n| `sidebar: { hide: true }` (hides the sidebar on this page; it doesn't take the page out of one) | `mode: center`. Not `sidebar.hidden`, which removes the page from the navigation, search, and the sitemap |\n| `sidebar: { path }` (which sidebar this page shows) | drop: the tab whose `path` holds the page decides its sidebar |\n| `navigation.nextButton` + `previousButton` `hide: true` | `pagination: false` |\n| `navbar`/`footer` hidden | `mode: custom` (the header only: no sidebar, title, or footer). The header always shows: report a hidden navbar |\n| `rbac` | drop; report the page as access-controlled |\n| `markdown.*`, `codeSnippet`, `breadcrumbs`, `colorMode`, `feedback`, `search`, `versionPicker`, `banner`, `template`, `keywords` (Typesense search curation), `seo.meta`/`jsonLd`/`lang`/`priority` | drop (report what was set) |\n| top-level `editPage` (including legacy `editPage: { disable }`) | drop without reporting: Realm reads only `markdown.editPage`, so it never had an effect |\n| custom keys (`metadata`, …) | `frontmatter.extend` if the site needs them, else drop |\n\n## API reference\n\nRealm renders every description in the content tree (a `.yaml`/`.json` with a top-level `openapi:`, `swagger:`, or `asyncapi:` key, or a `.graphql` schema) **at its path without the extension**: an overview at `/apis/orders/openapi`, a page per tag, and a page per operation below it. `apis` entries match files by `root` and carry lint and display settings; a description needs no entry to render. List every description, and report any `apis.*.root` that points at a missing file.\n\n- **Use `openapi()`** from `blume/reference`, one source per description: like Realm, it gives each operation its own page and URL. (`scalar()` would collapse each API onto one page.) AsyncAPI → `asyncapi()`; GraphQL → `graphql()` with `endpoint`. Leave the files where they are.\n- **Choose each `route`.** When the description shares its folder with guides (`apis/orders/openapi.yaml` beside `apis/orders/quick-start.md`), keep the old path (`route: \"/apis/orders/openapi\"`): its overview URL survives, and the reference sits in that folder's group beside the guides. **When the description is alone in its folder** (`apis/orders/openapi.yaml` and nothing else), the old path nests the reference's group inside a folder group that holds only it: use the folder as the route (`route: \"/apis/orders\"`), so the reference is that group. That moves one overview URL, which the catch-all below covers. Either way, `label` (the `sidebars.yaml` label, else the spec's `info.title`) names the group. Choosing a route inside a topic folder elsewhere moves the reference to where a `sidebars.yaml` had placed it, at the cost of more redirects; say which you chose. Add a `navigation.tabs` entry if an API was a top-level section.\n- **Spec problems fail `blume validate --strict`** (`BLUME_OPENAPI_*` warnings, e.g. path parameters declared `in: query` or missing). Fix them with an OpenAPI Overlay in the source's `overlays`, leaving a generated spec untouched, and report them for a fix at the source.\n- **Decorators don't run.** Realm applies `decorators` (`remove-x-internal`, `filter-out`, `info-override`), and the `preprocessors` and `plugins` behind them, when it bundles a description; Blume reads the raw file. When any are configured, point `spec` at `redocly bundle <api> --output <file>` output or express the change as an Overlay.\n- **Tag labels and order.** Blume reads `x-displayName` for each tag group's label, and orders tags as the spec's `tags` list does and operations as the spec lists them. `x-tagGroups` isn't read: when the spec has them and their flattened order (how Realm ordered tags) differs from `tags`, write `<route>/<tag-slug>/meta.ts` with `defineMeta({ order })`, the tag's position there (the folder under `content.root`, mirroring the route; it must sit in a folder `content.include` reaches, or `BLUME_META_OUTSIDE_INCLUDE` warns that it's never read). It merges over the generated title and order. Get each tag's folder name from Blume's routes (below), not by slugifying. `x-tagGroups` names drop, and tags left out of every group, which Realm hid, now appear: report both. `x-traitTag`, `x-logo`, and `x-badges` are ignored.\n- **Code samples**: `codeSamples.languages[].lang` (root and per-API `openapi:`) → `codeSamples`; Blume reads Redocly's names (`curl`, `JavaScript`, `Node.js`, `Python`, `Java`, `C#`, `PHP`, `Go`, `Ruby`). `R`, `Payload`, `Java8+Apache`, and `C#+Newtonsoft` have no generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: drop them and report. With no languages set, Realm showed its full default list and Blume shows `curl`, `js`, `python`: report the difference. `x-codeSamples` render natively. `codeSamples`, `playground`, and `expandSchemas` are `openapi()` options, shared by all its sources: APIs whose settings differ go in separate `openapi()` entries.\n- **Try it**: `hideReplay: true` → `playground: false`. Realm proxied Try it requests by default; Blume sends them from the browser. Set `playground: { proxy: \"<url>\" }` only for a proxy the user runs; **never carry over a Redocly-hosted one** (`/_api/cors/`, the default; `https://cors.redocly.com`; `https://cors.redoc.ly`). Probe each API's server for CORS with `curl -si -X OPTIONS -H 'Origin: https://<docs-host>' -H 'Access-Control-Request-Method: GET' <server-url>/<some-path>` and list the ones without a matching `access-control-allow-origin` as a follow-up: `playground: { proxy: true }` fixes them once the user picks a server-output host.\n- **Other display options**: `excludeFromSearch` → the source's `includeInSearch: false`; `schemasExpansionLevel: all` → `expandSchemas: true`; `layout`, download buttons, `showExtensions`, `feedback` (Blume's page rating shows on every overview and operation page, with no per-source switch), `mockServer`, and the rest drop (report).\n\n## URLs and redirects\n\n**Save the old URL list first**: a Realm site with `seo.siteUrl` serves `<site>/sitemap.xml`, listing every page **and every API overview, tag, `section/`, and operation page**. Every URL on it must end up a Blume page or a redirect to one.\n\n- **Page routes.** Realm slugs the file path: lowercased, spaces → `-`, `_` and `.` kept, **numeric prefixes kept**. Blume keeps the filename's case and its spaces, and strips a numeric prefix followed by `-`, `_`, or `.` (`01_setup` → `/setup`; a date like `2024-01` or a version stays whole). Rename mixed-case and spaced files to match; a numbered file needs `slug` (or a redirect) to keep its number; a nested `README.md` (Realm `/…/readme`, Blume `/…/README`) becomes `readme.md` to keep its URL.\n- **Moved pages** (materialized groups, `index` promotions, tabs) and **Realm's own redirects** (config, frontmatter `redirects`, extra `slug`s) all become `redirects`. A long generated list reads best in its own `redirects.ts` module, imported into `blume.config.ts`.\n- **Operation pages change shape.** Realm: `<old route>/<tag-slug>/<operationId lowercased>` (`getMultipleDomains` → `getmultipledomains`, `Domains.getDomain` → `domains.getdomain`), or `<old route>/<tag-slug>/paths/<JSON pointer>/<method>` with no `operationId` (`…/paths/~1pets~1%7Bid%7D/get`). Realm tag slugs keep underscores. Blume's slugs differ, so write **one exact redirect per operation**, generated:\n  1. `blume build` once with the references configured.\n  2. `node <skill>/scripts/operation-routes.mjs dist > operation-routes.json` prints, per reference route, every endpoint (`GET /pets/{id}`, or `POST petAdopted` for a webhook) mapped to its Blume route, read from the build itself (a Vercel server build's pages are in `.vercel/output/static`: pass that). `--spec <route>=<file.json>` adds operationIds (convert YAML first: `npx @redocly/cli bundle openapi.yaml --output openapi.json`).\n  3. For each old operation URL, find its operation in the spec: match the last segment against each `operationId` lowercased, or decode the pointer (`%7B` → `{`, `~1` → `/`, `~0` → `~`) to a path plus method. Look up `METHOD path` under the new route, and emit `{ from: <old URL path>, to: <route> }`.\n- **Tag, `section/`, and tag-description heading pages** (`<old route>/<tag>`, `<old route>/section/<heading>`, `<old route>/<tag>/<heading>`) don't exist in Blume: send them to the overview. When the route moved, one catch-all does it: `{ from: \"<old route>/:path*\", to: \"<new route>\" }` (exact operation entries still win). When the route stayed, use `{ from: \"<route>/section/:path*\", to: \"<route>\" }` and `{ from: \"<route>/:tag\", to: \"<route>\" }`, plus exact entries for tag headings; a pattern that covers a page fails the build (`BLUME_REDIRECT_MATCHES_PAGE`).\n- **Legacy hash links** (`#operation/<id>`, `#tag/<tag>`, `#section/…`) worked on Realm, which resolved them in the browser, and still appear **in content, partials, and the description files' own descriptions**. A server can't redirect a fragment. `blume validate` flags one on a page that still exists as a broken anchor, but accepts any redirect `from` as a link target, so it misses the ones whose path now redirects: `grep -rn '#operation/\\|#tag/' <content> <spec files>` and rewrite each to its Blume route. Rewrite absolute links to the site itself (`https://<docs-host>/…`) to root-relative paths too; `blume audit` flags the ones left.\n- **Trailing slashes.** Realm answered `/a/` with a 301 to `/a`, and so do `blume dev` and `blume preview`, for pages and redirect sources alike. In production it depends on the host: static hosts serve a page's `a/index.html` at `/a/`, and server builds (`vercel()`, `node()`) match `/a/` for a redirect `from`, but whether a static host applies a redirect rule to the slashed form varies. When old links used trailing slashes (frontmatter `redirects` often did), check one slashed redirect on a preview deployment and report it.\n- **Versions and locales**: Realm drops the `@` from version folders (`api/@v1/x.md` → `/api/v1/x`) and serves the default version without its segment; non-default locales get the lowercased code (`@l10n/es-ES/` → `/es-es/…`).\n- **Check coverage**: after the build, every old URL should be a page in `dist/` (`<path>/index.html`) or resolve through `dist/blume-redirects.json` (exact entries first, then patterns) to one. That manifest is written for a static build with no host adapter; with one, request each URL from a running `blume dev` (or `blume preview` of a static build) instead: both answer every redirect, patterns included. `blume audit --only redirects` checks the redirects themselves.\n\n## Assets, theme, React pages, and the repo\n\n- **`static/` → `public/`**: Realm serves `static/` at the root, so root-relative references keep working. Drop a Redocly-hosting `robots.txt` (`/~/`, `/cdn-cgi/` rules); Blume writes its own.\n- **`.gitignore`**: Realm projects often list `public/` (and sometimes `_partials`), which would leave moved files uncommitted: remove those lines. Add `.blume/`, `dist/`, and `.env.local`, as `blume init` does.\n- **Node**: Blume needs Node 22.19 or later, and Realm templates often pin 20 in `.nvmrc`. Update `.nvmrc`/`.node-version`, `engines.node`, and CI `node-version`.\n- **`@theme/styles.css`**: the brand color (`--color-primary-base` or the variable it points at) → `theme.accent`; check it against dark mode, which `blume audit` checks for contrast. Rules for the site's own classes (a landing page, figures) move to a root `theme.css`, with `html.dark` selectors rewritten as `[data-theme=\"dark\"]`. Rules for Realm's components don't apply: report them.\n- **`@theme/components/*`**: a component a page uses → an island (`islands/`, or imported by a custom page with `client:*`); an ejected Realm component → the matching layout slot if one exists, else drop. `@theme/layouts`, `Templates`, `plugin.*`, and `markdoc/` drop once their tags are converted.\n- **`*.page.tsx`**: `index.page.tsx` → `pages/index.astro` on `PageLayout`, with interactive parts as React islands, or an `index.mdx` with `mode: custom` when it's mostly links. Its `export const frontmatter` gives the title. `404.page.tsx` → `pages/404.astro`, or Blume's built-in 404. A React-state light/dark image swap becomes two images with CSS hiding one.\n\n## i18n\n\n`l10n: { defaultLocale, locales: [{ code, name }] }` → `i18n: { defaultLocale, locales: [{ code, label: name }] }`. Pages under `@l10n/<code>/` move to `<code>/` at the content root. Blume uses `code` as written in URLs, so write it lowercase (`es-es`, folder too) to keep Realm's URLs. `translations.yaml` label keys → per-locale `meta.ts` titles and tab label maps; UI strings → `i18n.ui` where Blume has one; report the rest.\n\n## Versions\n\nRealm versions a folder through `@<version>/` subfolders and an optional `versions.yaml` (`default`, order; with no `default`, the last listed version is the default); the default version serves without its segment. Blume's versioning is site-wide:\n\n- **Version folders at the content root** → Blume versioning: the default version becomes the current tree (`versions.current: { label }`, required), the others top-level folders listed newest first in `versions.archived` as `{ id, label? }` (ids start with a letter: `@1.0` → `v1.0`, redirect `/1.0/:path*`).\n- **A versioned section** (`api/@v1/`) → plain folders reproduce the URLs: the default version's files move up into `api/`, the others into `api/v1/` (a name like `1.0` stays whole). Add a `navigation.selectors` entry `{ kind: \"version\", … }` and report the missing old-version notice and version-scoped search. Versioned descriptions → one `openapi()` source each.\n\n## Icons\n\nRealm icons are Font Awesome names (`book`, `solid check-circle`, `brands github`) or image paths. Strip the pack prefix and map to Lucide with the table in `references/mintlify.md`; image paths work as written; brand icons other than the few Lucide ships (listed there, `github` among them) have no Lucide match (report).\n\n## Teardown — keep lint and bundle working\n\n1. **Trim `redocly.yaml`, don't delete it**: remove the portal keys and keep `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, and each `apis` entry's `root`, `output`, `overlays`, and lint keys (drop its `openapi`/`graphql`/`theme` display keys). If nothing but `apis` roots remain, keep it anyway when scripts or skills run `redocly lint`; otherwise ask. Run the lint script before and after: the result should match.\n2. Keep `@redocly/cli` and its scripts and CI jobs. Remove the Realm-family packages, `styled-components` and the other template peers nothing else imports (keep `react`/`react-dom` if an island uses them), and the `realm` scripts; add `blume` and the `blume dev`/`build`/`preview` scripts.\n3. Delete `sidebars.yaml` files, `versions.yaml`, `@theme/`, the React pages once rebuilt, `translations.yaml` once mapped, the `tsconfig.json`/`types.d.ts` that only served them, and the redirects file a `$ref` pointed at.\n4. Reunite settings (custom domain, environment variables, teams, hosted analytics) live outside the repo: list what the user must recreate.\n\n## Dropped — report these\n\nLogin, RBAC, and SSO (every protected path); Reunite's editor, reviews, previews, and feedback dashboard; the API explorer's mock server, environments, saved auth, and default CORS proxy (and which APIs now fail CORS); `x-tagGroups` names and the tags they hid; API tag and `section/` pages (redirected); sidebar separators and `expanded: always`; footer column labels; the ` - <title>` suffix now on every `<title>`; dropped frontmatter titles; `seo.image`/`jsonLd` and `llms.txt` sections; extra banners; search facets; resolved `if` branches and per-reader content; `code-snippet` ranges; the Markdoc tags with no equivalent; PlantUML/Excalidraw; light/dark image pairs; unmatched icons; ejected components and Realm CSS; a `UA-` analytics id; scripts that now load only in production; trailing-slash URLs; the accidental pages left out; reset \"last updated\" dates.\n",".agents/skills/blume-migrate/references/starlight.md":"# Starlight → Blume\n\nStarlight is an Astro integration configured via `starlight({…})` in `astro.config.*`, with content in `src/content/docs`. Both are Astro, so this is a natural fit — content can mostly stay in place, with one big caveat: **most Starlight content is `.md`, and Blume's `:::` directives are MDX-only** (see Asides below).\n\n## Detect\n\n- `astro.config.{mjs,mts,ts,js,cjs}` importing and calling **`starlight({…})`** (`import starlight from \"@astrojs/starlight\"`).\n- Content under **`src/content/docs/`**; a `src/content.config.ts` (current) or `src/content/config.ts` (pre-Astro-5).\n- `@astrojs/starlight` dep.\n\nKeep content where it is — set `content.root: \"src/content/docs\"`.\n\n## Config: `starlight({…})` → `blume.config.ts`\n\n**Harvest the surrounding `astro.config.*` too, not just the `starlight()` call:** top-level Astro `redirects` → Blume `redirects`; `site` → `deployment.site`, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md); other integrations → report.\n\n| Starlight option | Blume |\n| --- | --- |\n| `title` | `title` (if per-locale/computed, set manually) |\n| `description` | `description` |\n| `logo` (`{ src }` or `{ light, dark, alt }`) | `logo` — move the referenced file into `public/` |\n| `logo.replacesTitle` | `logo: { text: \"\" }` (renders the mark alone) |\n| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |\n| `social` (array of `{ label, icon, href }`; pre-0.33 legacy: `{ github: url }` object) | derive **`github: { owner, repo }`** from the GitHub entry; every other entry whose platform Blume knows → **`footer.socials`** (`{ discord: href, bluesky: href, … }`; the `x.com`/`twitter` icon → `x`; the platform list is under `footer` in `docs/configuration/index.mdx`); the rest (`mastodon`, `gitlab`, `rss`, …) drop (report) |\n| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it); **an origin other than `https://github.com` → `github.host`** (a GitHub Enterprise repo otherwise links to the public site) |\n| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |\n| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |\n| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |\n| `expressiveCode.themes` | `markdown.code.theme: { light, dark }` — EC assigns by theme _type_, so match each theme by its darkness, not by array position |\n| `expressiveCode.styleOverrides` | drop → restyle via a root `theme.css` file |\n| `head` | per entry: a `meta` tag → **`seo.metatags`** (`{ [attrs.name ?? attrs.property]: attrs.content }` — verification tokens, `theme-color`, and the like), minus the tags Blume writes itself (`description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, `article:*` dates — the build refuses them and names the setting that owns each, e.g. `og:image` → `seo.og`, `twitter:site` → `seo.x.handle`); an analytics `<script>` → one `script()` adapter each in the `analytics` list (from `blume/analytics`): `attrs.src` → `src`, an `async`/`defer` attr → `strategy`, every other `attrs` key → `attributes`, and an inline body (`content`) → `content`; anything else (`link`, `style`, other scripts) → drop and report |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `customCss` | drop → move into a root **`theme.css`** file (auto-picked-up; not a config field) |\n| `components` (overrides) | Blume's layout slots via `defineComponents({ layout: { … } })` in a root `components.ts`: `Header`, `Search`, `Sidebar`, `TableOfContents`, `Pagination` keep their names; `SiteTitle` → `Logo`; **`Footer` → `PageFooter`**, not Blume's `Footer` (that slot is the site footer; Starlight's `Footer` is the page-end last-updated date, pagination, and edit link, which Blume renders itself, so keep only what the override added). Other overrides (`Banner`, `PageTitle`, `Hero`, `EditLink`, `SocialIcons`, …) have no slot — fold their additions into `PageHeader`/`PageFooter`, or `blume eject`; a Starlight name left under `layout` matches no slot and does nothing. Rewrite reads of `Astro.locals.starlightRoute` against the slot's props |\n| `plugins` | **map, don't blanket-drop** — see Plugins below |\n| `pagination: false`, `pagefind`/Pagefind options, `titleDelimiter`, `credits`, `disable404Route`, `routeMiddleware` | drop (report) |\n| `locales` / `defaultLocale` | `i18n` (see below) |\n\n## Navigation: `sidebar`\n\nStarlight's `sidebar` array → prefer letting Blume generate from the filesystem; use `navigation.sidebar` only for shapes files can't express:\n\n- `{ label, items: [{ autogenerate: { directory: \"d\" } }] }` (0.39 and later; before 0.39 the group held it directly, `{ label, autogenerate: { directory: \"d\" } }`) → **structure the folder and rely on filesystem nav** (label → the folder's `meta.ts` `title`; `autogenerate.collapsed` → `collapsed: true` in each subfolder's `meta.ts`). Do **not** map it to a sidebar item with `root:` — in an explicit Blume sidebar that renders a single page link, not the directory's children. Since 0.39 a group's `items` can mix manual entries with an `autogenerate` entry; move those manual pages into the folder (route change → `redirects`) so the folder holds the whole group.\n- string `\"guides/intro\"` → `\"/guides/intro\"`; `{ label?, slug }` → `/<slug>`.\n- `{ label, items: [...] }` → `{ label, items: [...] }` (recursive) — or better, a real folder.\n- `{ label?, link }` → `{ label, href: link }`.\n- `badge` (string or `{ text, variant }`) → `badge` (text only; variant drops). `collapsed` (bool) → `collapsed`. Item `attrs` and `translations` → drop (report).\n\n## Frontmatter\n\n| Starlight | Blume |\n| --- | --- |\n| `title` / `description` / `draft` / `slug` | pass through |\n| `sidebar.{label,order,hidden}` | pass through |\n| `sidebar.badge` | `sidebar.badge` (text only) |\n| `sidebar.attrs` | **remove** (strict schema — build error if left) |\n| `pagefind: false` | `search.exclude: true` |\n| `lastUpdated` (date/string) | `lastModified` |\n| `lastUpdated: false` (boolean) | **remove** (Blume's `lastModified` takes a date; a boolean is a build error) |\n| `prev: false` **and** `next: false` | `pagination: false` (hides both links) |\n| `prev` / `next` otherwise (one side, or a label/link) | **drop** (report — the links can only be hidden together, and their labels come from the linked pages) |\n| `template: splash` | `mode: center` (no sidebar, a wider centered column — the closest layout; report) |\n| `hero` | **no key** — its tagline → a paragraph, its actions → `<Card>`s or links, its image → a Markdown image (report); for a designed landing page, rebuild it as a custom `.astro` page under `content.pages` (`pages/index.astro` on `PageLayout` wins over the content page) |\n| `banner`, `tableOfContents`, `editUrl`, `head` | drop (report) |\n\n## Asides → directives (and the `.md` trap)\n\nStarlight's primary callout syntax is the `:::note`/`:::tip`/`:::caution`/`:::danger` directive **in plain `.md` files**. The directive names map perfectly (Blume aliases `caution`→warning; `error`→danger) and `[Title]` syntax carries over — **but Blume only parses directives in `.mdx`**. In a `.md` file, `:::note` renders as literal text and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. So: **rename every `.md` file that contains asides (or math, or mermaid/package-install fences) to `.mdx`** — for a typical Starlight repo that's most of the content; renaming everything to `.mdx` is usually simpler and safe.\n\n- `<Aside type=\"…\" title=\"…\">` (the component form) → the same directives; bare `<Aside>` → `:::note`.\n- An aside custom icon (`:::tip{icon=\"heart\"}`) → drop the attr (report).\n\n## Components\n\n- **Renames:** `<CardGrid>` → `<CardGroup>`; `<LinkCard>` → `<Card>` (its `description` prop drops — fold into the body); `<TabItem label=\"…\">` → `<Tab title=\"…\">`. `<Tabs>` and `<Card>` stay; **keep** a `<Tabs syncKey=\"…\">` prop as is — Blume's `Tabs` takes `syncKey` with the same scoping (only groups sharing the key switch together). One difference: Blume groups without a key also sync, page-wide, by tab title, where Starlight leaves them independent — add `sync={false}` to a keyless group whose same-titled tabs must stay unlinked.\n- **`<Badge>` needs conversion, not pass-through:** Starlight puts content in a `text` prop and uses variants `note`/`tip`/`caution`/`danger`/`success`/`default` with sizes `small`/`medium`/`large`. Blume's `<Badge>` renders **children** with variants `default`/`accent`/`success`/`warning`/`danger` and sizes `xs`/`sm`/`md`/`lg`. Move `text` into the children; remap variant (`note`→`default`, `tip`→`accent`, `caution`→`warning`, `danger`→`danger`, `success`→`success`) and size (`small`→`sm`, `medium`→`md`, `large`→`lg`).\n- **Convert yourself:** `<Steps>` → Blume `<Steps>`/`<Step>`; `<FileTree>` → Blume `<FileTree>`; `<Code code={…}>` → a fenced code block; `<LinkButton>` → a Markdown link or `<Card>`.\n- Strip `import … from \"@astrojs/starlight/*\"` and `astro:assets` lines.\n\n## Code blocks: Expressive Code meta → Blume\n\nStarlight content is full of Expressive Code fence meta; Blume understands some of it and **promotes unknown bare tokens into the code-block title**, so unconverted meta produces garbage headers. Convert per fence:\n\n- `title=\"file.js\"` → works as-is (or use the space-title shorthand). Line ranges `{2-3}` → work as-is.\n- `ins=`/`del=` line marks → `// [!code ++]` / `// [!code --]` comments; `mark=` → `{ranges}` or `// [!code highlight]`.\n- `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left).\n- `wrap` / `wrap=true` → a bare `wrap` (that block's long lines wrap); a site-wide `expressiveCode.defaultProps.wrap: true` → `markdown.code.wrap: true`.\n- **Drop:** `frame=\"terminal\"`, `collapse=`, `wrap=false`, `preserveIndent`/`hangingIndent`, `\"string\"` and `/regex/` text markers (report if they carried meaning).\n- ` ```diff lang=\"js\" ` → a normal ` ```js ` fence with `[!code ++]`/`[!code --]` markers.\n\n## Plugins — map, don't drop\n\n- `starlight-openapi` → an `openapi({ sources })` entry in Blume's `reference` list, imported from `blume/reference` (delete any generated pages; add the `navigation.tabs` entry).\n- `starlight-blog` → `type: blog` pages (RSS at `/blog/rss.xml`). Blume generates **no** blog index, tag, or author pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag and author pages as dropped.\n- `starlight-versions` → Blume's native versioning, not a `navigation.selectors` dropdown: each archived version's content goes in a top-level folder under `content.root` named for its id, listed in `versions.archived` (newest first), and Blume adds the switcher, the old-version notice, and version-scoped search. Ids must start with a letter (`1.0/` → `v1.0/`, with `redirects` from the old URLs), and a version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as current content. Full reference: `docs/content/versioning.mdx` in the installed package.\n- `starlight-image-zoom` → delete (Blume zooms content images by default).\n- `starlight-links-validator` → delete (`blume validate` covers it).\n- Anything else → report.\n\n## Assets\n\nStarlight co-locates images in `src/assets/` with **relative** references (`../../assets/foo.png`) or `astro:assets` imports; Blume serves `public/` at the site root with absolute URLs. Move `src/assets/*` into `public/`, rewrite relative image paths and `~/`/`@/` aliases to absolute `/…` URLs, and replace `<Image>` imports with Markdown images (or `<Frame>`).\n\n## i18n\n\nStarlight has **two** layouts:\n\n- **`root` locale** (default language at `src/content/docs/`, others in `fr/`, `de/`… subdirs) → matches Blume's `dir` parser as-is: `i18n: { defaultLocale, locales: [{ code, label }] }`, no file moves.\n- **No `root` locale** (every language in a subdir, including the default — `en/…`) → Blume expects the default locale **at the content root**, so move the default locale's files up one level. Starlight served them at `/en/…`, so add a `redirects` entry per page.\n\nDon't restate Blume defaults (`hideDefaultLocalePrefix: true`, `parser: \"dir\"` are already the defaults). Starlight's untranslated-page fallback matches Blume's `fallbackLocale` default.\n\n## Teardown\n\nRemove `@astrojs/starlight` (and plugin deps) from deps, delete the Starlight bits of `astro.config.*` **after harvesting redirects**, delete `src/content.config.ts` / `src/content/config.ts`, repoint scripts to the Blume CLI, add `blume`. A `src/content/docs/404.md` has no direct Blume equivalent — report it (Blume ships its own 404).\n\n## Dropped — report these\n\nSocials on a platform `footer.socials` lacks, badge variants, sidebar/item `attrs` + `translations`, `customCss` beyond `theme.css`, `head` entries other than meta tags and analytics scripts, a page's frontmatter `head`, `routeMiddleware`, splash layouts approximated with `mode: center` and hero blocks rebuilt as content, override components with no Blume slot, starlight-blog's tag and author pages, aside custom icons, EC frames/collapse/text markers, prev/next toggles, unmapped plugins, any `<Icon>` name with no Lucide equivalent.\n",".agents/skills/blume-migrate/references/vitepress.md":"# VitePress → Blume\n\nVitePress is a Vue/Vite static site generator. Its config is a module in `.vitepress/`, navigation is **declared in `themeConfig.sidebar`**, every `.md` page compiles as a **Vue single-file component** (so it can hold Vue components, `{{ }}`, and `<script setup>`), and its Markdown is markdown-it plus VitePress plugins. Watch for five traps:\n\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns (`BLUME_DIRECTIVE_SPACED_NAME`) only for a callout name in `.mdx`. Inside an open `:::` container, the same line closes the container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`). Directives are also MDX-only.\n- **Every page is `.md`, but most need to be `.mdx`.** A page with a callout, component, or math must be renamed, and MDX is stricter than Vue templates.\n- **URLs end in `.html` by default** (`cleanUrls: false`), so every page needs a redirect.\n- **The heading is the body `# H1`, not frontmatter.**\n- **Pages missing from the sidebar are hidden in VitePress.** In Blume they appear unless you hide them.\n\nScope: VitePress 1.x. Notes marked **2.0** cover `2.0.0-alpha.*`.\n\n## Detect\n\n- **A `.vitepress/` directory** holding `config.{js,ts,mjs,mts}` (or `config/index.*`), usually `docs/.vitepress/`. The directory that holds `.vitepress/` is the VitePress **project root**: the argument to `vitepress dev <dir>` in the scripts.\n- **A `vitepress` dependency**, often only in the docs package's `package.json`.\n- **Not VuePress**, which uses `.vuepress/` and `vuepress`/`@vuepress/*` deps (`references/vuepress.md`).\n\n**Where the Blume project goes.** VitePress has a project root, a `srcDir` (the pages, default `.`), and a public directory that is always `<srcDir>/public`. Blume serves `public/` from the directory holding `blume.config.ts`. Two layouts are common:\n\n- The scripts run `vitepress dev docs` from the repo root. Put `blume.config.ts` at the repo root; the default `content.root: \"docs\"` matches (append `srcDir` if one is set). Move `docs/public/` to `public/`.\n- `docs/package.json` runs `vitepress dev`. Put `blume.config.ts` in `docs/`, set `content.root: \".\"`, and scope `include`/`exclude` (`references/monorepo.md` §1). `public/` stays put, but exclude it (VitePress sites keep `.md` there behind `srcExclude`), along with `node_modules`.\n\n## Run the codemod first\n\nBuild the source once (`vitepress build <root>`) and copy `.vitepress/dist` outside the repo (`<old build>` below; skip the build if the user already made one): it's your old URL list, the old heading ids, and the record of what the live site rendered, and Teardown deletes `.vitepress/`. Then, on the VitePress source directory (Blume's `content.root`), dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/vitepress-codemod.mjs <srcDir>          # report only\nnode <skill>/scripts/vitepress-codemod.mjs --write <srcDir>  # apply\nnode <skill>/scripts/vitepress-codemod.mjs --redirects <old build>  # `.html` redirects as JSON\n```\n\nIt's zero-dependency and idempotent. It converts only where the mapping is exact: containers (paired as markdown-it pairs them, see Markdown extensions), code groups, `details`, GitHub alerts, fence `[label]`s and meta, `<<<` snippet imports and `<!--@include:-->` without regions or line ranges, `.html`/`.md`/trailing-slash links backed by a page, `<Badge type text />`, `{#id}`, `[[toc]]`, and the body H1 → `title`. It renames a page to `.mdx` when the result needs MDX (partials keep their names) and makes its comments and `<br>` MDX-safe. **Everything else is a `REVIEW` line**: frontmatter keys Blume rejects, components, Vue syntax, regions and out-of-root snippets, repaired container structure, unconverted includes (including ones inside code fences, which VitePress spliced). Work through that list with the sections below; it's the migration's to-do list.\n\n## Config: `.vitepress/config.*` → `blume.config.ts`\n\nRead the whole module and the helpers it imports (sidebars, nav, per-locale configs). For **2.0**, also read directory-level `config.*` files beside the content.\n\n| VitePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `titleTemplate` | drop (report). Blume's `<title>` is `<page> - <site>`; `seo.title` replaces a page's part |\n| `lang` (not `en-US`) on a single-language site | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`): it sets `<html lang>` and the UI strings, with no switcher or prefix. Use a `code` with a UI pack |\n| `head` | per entry: a known analytics snippet → **that provider's factory** from `blume/analytics` (the gtag loader + `gtag('config', 'G-…')` pair → one `googleAnalytics({ id })`; also GTM, Plausible, …); any other `<script>` → `script()` (`src`, `async`/`defer` → `strategy`, other attrs → `attributes`, inline body → `content`). A `meta` tag → `seo.metatags`, minus the tags Blume writes itself (`description`, `robots`, `og:*`, `twitter:*` card tags; the build names the setting that owns each). `link rel=\"icon\"` → a conventional favicon name in `public/`. Fonts and other tags → drop (report) |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `cleanUrls` | no setting; see \"URLs and redirects\" |\n| `rewrites` | **move each file to the path its rewrite produced**, so the URL stays put (`slug` only where moving is impossible). VitePress relative links are already written against the rewritten paths |\n| `srcDir` / `srcExclude` | `content.root` / `content.exclude`, which adds to the defaults `**/_*` and `**/.*` |\n| `outDir`, `assetsDir`, `cacheDir`, `ignoreDeadLinks`, `metaChunk`, `mpa` | drop |\n| `appearance` | `'dark'`/`'force-dark'` → `theme.mode: \"dark\"`; `false` → `\"light\"`; `true`/`'force-auto'` → omit. Blume always shows the toggle: report a forced mode |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `sitemap.hostname` | `deployment.site`, unless the host is Vercel or Netlify (auto-detected). `sitemap.transformItems` → drop |\n| `markdown.theme` | `markdown.code.theme: { light, dark }`; VitePress's default (`github-light`/`github-dark`) is Blume's, so omit it unless changed |\n| `markdown.lineNumbers: true` | no site-wide switch: add `lineNumbers` per fence, or drop (report) |\n| `markdown.math` | delete `markdown-it-mathjax3`; see Math |\n| `markdown.container` labels, `anchor`, `toc`, `attrs`, `image`, `languages`, `languageAlias`, `headers` | drop (report). A custom `anchor.slugify` changes every anchor |\n| `markdown.config(md)` / `preConfig(md)` with `md.use(…)` | **custom markdown-it plugins: find the syntax each one adds in the content and convert every use** (to Markdown, a directive, or a component); the codemod doesn't know it. Report each plugin |\n| `markdown.codeTransformers` with `@shikijs/vitepress-twoslash` | drop the plugin: ` ```ts twoslash ` works natively |\n| `markdown.externalLinks` | VitePress opens external links in a new tab by default; `markdown.externalLinks: true` keeps that (optional) |\n| `vite`, `vue`, build hooks (`transformHead`, `transformPageData`, `buildEnd`, …) | drop (report what each did). Blume writes canonicals, OG images, `llms.txt`, and the sitemap itself; Astro integrations go in `integrations` |\n| `locales` | `i18n` (see below) |\n\n**`themeConfig`** (default theme):\n\n| `themeConfig` | Blume |\n| --- | --- |\n| `logo` (string, `{ src, alt }`, `{ light, dark, alt }`) | `logo` (string, or `{ image: { light, dark, alt } }`), file in `public/`. A monochrome SVG → `currentColor` and the string form |\n| `siteTitle: false` / a string | `logo: { image: …, text: \"\" }` / `logo.text` |\n| `nav`, `sidebar` | see the Navigation sections |\n| `socialLinks` | GitHub → `github: { owner, repo }` (it renders in the footer); known platforms → `footer.socials` (`twitter` → `x`; list under `footer` in `docs/configuration/index.mdx`); others, including custom `{ svg }` icons → `footer.links`, or drop (report) |\n| `footer` (`message`, `copyright`) | `copyright` → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`); a per-locale `copyright` → a locale map. `message` → drop (report); `footer.links` takes `{ label, href }` |\n| `editLink.pattern` (`…/edit/<branch>/<path>/:path`) | `github: { owner, repo, branch }`. `<path>` leads to the **srcDir**, but Blume builds edit links from the Blume project dir, so `github.dir` is `<path>` minus `content.root` (omit when empty). A non-`github.com` origin → `github.host` |\n| `lastUpdated.formatOptions` | its date fields → `dateFormat`; `text` → drop |\n| `search` | `local` → omit. **Algolia → omit `search`** (Blume's built-in Orama), remove the DocSearch deps, and report the `appId`/`indexName`. That index is almost always DocSearch's, filled by Algolia's crawler, and Blume's sync, given an admin key, replaces the whole index on every build: never point `algolia()` at it. `search: algolia({ appId, apiKey, indexName })` from `blume/search` is for a new index Blume owns, a separate opt-in |\n| `outline` (`2`, `[2, 4]`, `'deep'`, `false`) | `toc` (`{ minHeadingLevel, maxHeadingLevel }`; `'deep'` → 2–6; `false` → `toc: false`), only when declared |\n| `aside`, `docFooter`, the `*Label` strings, `externalLinkIcon`, `carbonAds`, **2.0** `gradedContainers`, theme-package keys | drop. UI strings come from Blume's i18n packs (`i18n.ui`) |\n\n## Navigation: `sidebar`\n\nVitePress shows only what `sidebar` lists, in that order. Make the file tree match it, carrying labels, order, and collapse into `meta.ts`; reach for `navigation.sidebar` only for shapes files can't express.\n\n- **Multiple sidebars** (`{ '/guide/': [...] }`) → one **`navigation.tabs`** entry per key (`{ label, path: \"/guide\" }`, label from the `nav`). Give each tab folder a `meta.ts` `title` matching its tab label. VitePress sidebars rarely list root pages, so once those are hidden, a root route has no loose pages to show and Blume falls back to the full tree, where an untitled tab folder shows its humanized folder name.\n- **A group whose pages sit flat in one folder** (the nesting exists only in config) → **a `(group)` folder**: `docs/(fundamentals)/controllers.md` still serves `/docs/controllers`, so no URL changes and no redirects. Give it a `meta.ts` (`title`, `pages` in the listed order). A listed page from another folder can join with frontmatter `slug` keeping its old route. **Moving a page one folder deeper breaks its relative links and assets** (`./assets/x.png`, `../other.md`): re-base them, or make them root-absolute.\n- **`collapsed`**: unset → not collapsible (Blume's `flat`); `false` → `display: \"group\"` + `collapsed: false`; `true` → `display: \"group\"` + `collapsed: true`, in that folder's `meta.ts`.\n- **A group with its own `link`** → that page becomes the folder's `index` (its URL changes: redirect the old one).\n- **Item `text` that differs from the page's title** → `sidebar.label`. **`base`** → prefix the child links before mapping. External items → `navigation.featured`. `docFooterText`, `rel`, `target` → drop.\n- **Pages no sidebar lists** → `sidebar.hidden: true`. A page listed in two sections, or a cross-section link, can't repeat in filesystem nav: keep one place and report.\n\n## Navigation: `nav`\n\n- A link to a section a sidebar key backs → a `navigation.tabs` entry. **A dropdown whose items mirror that section's sidebar groups** (a mega-menu) → a plain tab: the sidebar already shows them.\n- A dropdown of pages in one section that isn't the sidebar → a tab with `items` (flat; nested groups flatten).\n- External or utility links → `navigation.actions`; a primary call to action → `navigation.cta`. A version dropdown → drop its version links (report), but move its utility links (Releases, Team, Contributing) to `footer.links` or `navigation.actions`, so pages linked only from there stay reachable.\n- `activeMatch`, `target`, `rel`, `noIcon` → drop.\n\n## Titles\n\nVitePress takes a page's heading from its first `# H1`; frontmatter `title` only sets `<title>`. Blume renders frontmatter `title` as the H1, so **a body H1 beside a `title` shows twice**; a page with no `title` takes it from the `# H1` that opens its body and shows it once. The codemod moves a plain H1 into `title` and deletes it. It reports two cases: an H1 with inline markup (move it by hand), and a frontmatter `title` that differs from the H1 (make the H1 `title`, keep the old value as `seo.title`). A home page with no H1 takes `hero.name` / `hero.text`.\n\n## Markdown extensions\n\nVuePress shares the container and fence conventions; its snippet and include syntax differ (see `references/vuepress.md`). **What Blume renders below is MDX-only unless marked `.md` OK.** Of these targets, plain `.md` handles fence titles and meta, `// [!code …]` notations, `{#id}`/`[#id]` anchors, and `<include>`; callouts, components, and math need `.mdx`.\n\n**Container pairing.** markdown-it-container scans each container's lines for its closer, so **a bare `:::` closes the outermost open container whose fence is no longer than it, and everything nested in it**. An unclosed container runs to the end of its parent (the page, for a top-level one), and a `:::` with nothing to close shows as text. Real sites have unclosed code groups that swallow the next callout, closers that close two containers, and stray closers. The codemod pairs this way, ends a code group at its last code block when other content follows inside it (a group only holds code), drops stray closers, gives a nested callout's outer fence the extra colon Blume needs, and reports every repair. Check the old build's HTML for literal `:::` to see what the live site already showed broken.\n\n| VitePress | Blume |\n| --- | --- |\n| `::: tip` / `info` / `warning` / `danger` (spaced) | `:::tip` etc., with **no space**; `::: tip` is never parsed (codemod) |\n| `::: danger STOP` | `:::danger[STOP]`. `:::danger STOP` renders without `STOP`, and `.mdx` warns `BLUME_DIRECTIVE_OPENING_TEXT` (codemod) |\n| `::: note` / `important` / `caution` | **2.0** containers → `:::note`, `:::note`, `:::danger` (`:::warning` with `gradedContainers`). On **1.x** they aren't containers and showed as text; converting changes that: report (codemod) |\n| `::: details Title` (`{open}`) | `<Expandable title=\"Title\">` (`defaultOpen`); untitled → `title=\"Details\"` (codemod) |\n| `> [!NOTE]` `[!TIP]` `[!IMPORTANT]` `[!WARNING]` `[!CAUTION]` (+ `[!INFO]`, `[!DANGER]`) | `:::note` / `tip` / `note` / `warning` / `danger` / `info` / `danger`; text after the marker → `[Title]` (codemod). Unconverted, the five GitHub markers still render as those callouts in `.mdx` (a lazy continuation included); in `.md` they're plain quotes with a literal `[!NOTE]` that warn `BLUME_MD_GITHUB_ALERT` (rename the page to `.mdx`), and `[!INFO]`/`[!DANGER]` are plain quotes in both, with no warning. The codemod leaves lazy-continuation and indented alerts for you |\n| `::: raw`, `::: v-pre` | remove the wrapper lines (codemod) |\n| `::: code-group` + `[label]` fences | `<CodeGroup>`; each block's title is its label (bare token, or `title=\"Two words\"`), else its language as VitePress showed. Blume syncs same-titled tabs page-wide (codemod) |\n| Raw `<div class=\"tip custom-block\">…</div>` (hand-written container markup) | the matching directive |\n| ` ```js{4} ` / ` ```ts:line-numbers ` / ` ```js [file.js] ` | ` ```js {4} ` (Blume reads it glued too) / ` ```ts lineNumbers ` (`=N` start → drop) / ` ```js file.js ` (a bracketed title also shows without its brackets; `.md` OK; codemod) |\n| `// [!code ++]` `--` `focus` `highlight`/`hl` `error` `warning` | same (`.md` OK) |\n| `js-vue` | plain `js`; resolve its `{{ }}` by hand |\n| `<<< @/path/file.ts{2,4} [Label]` (`{lang}`, `:line-numbers`) | `<include meta=\"Label {2,4}\">/path/file.ts</include>` (`lang=\"…\"`, `lineNumbers`); `@` is the srcDir, `/` Blume's content root. In a code group VitePress titles an unlabeled import with its file name. A `.md` target gets `lang=\"md\"`: VitePress showed it as code, Blume would splice it (`.md` OK; codemod) |\n| `<<< file#region` | no region selection in `<include>`: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (selects `#region` markers; set `\"dedent\": true`, since VitePress always dedents regions; runs before every build, so the excerpt keeps tracking the source). Where VitePress couldn't find the region it showed the whole file, so check the old build |\n| a snippet outside the content root (`@/../packages/…`) | `BLUME_INCLUDE_OUTSIDE_ROOT` fails the build: copy the file under `_snippets/` and report |\n| `<!--@include: ./part.md-->` | `<include>./part.md</include>` (`.md` OK; codemod). A line range or region → split the partial. VitePress publishes a partial as a page unless `srcExclude` hides it: prefix partials with `_` (excluded by default) and update the paths. A partial is parsed in the including page's format, so one spliced into `.mdx` must be MDX-safe |\n| `[[toc]]` | delete; Blume has the outline rail (codemod) |\n| `:tada:` emoji shortcodes | paste the emoji. A trailing emoji leaves no dash in the id (`## Emoji 🎉` → `#emoji`); the anchor script pins any old id that differs |\n| `## Heading {#id}` | `[#id]` in both formats; a bare `{#id}` fails `.mdx` (codemod) |\n| markdown-it-attrs (`[link](/x){target=\"_self\"}`, `{.class}`) | delete (codemod, for links) |\n| `$…$` / `$$…$$` (`markdown.math`) | `$$…$$` in `.mdx`, block or inline; convert each inline `$…$`, checking it's math |\n| ` ```mermaid ` (`vitepress-plugin-mermaid`) | as written in `.mdx`; delete the plugin |\n| `:::tabs` + `== Label` (`vitepress-plugin-tabs`) | `<Tabs>` of `<Tab title=\"Label\">`, or `<CodeGroup>` when every tab is one fence |\n| Containers a site registers itself (markdown-it-container in `markdown.config`, **2.0** `customContainers`) | read the registration, then map each to a callout, `<Tabs>`, or a component (report). The codemod pairs them as containers and leaves them as written, lengthening an enclosing callout's fence: Blume ends a `:::` callout at a `::: name` line inside it and drops the line |\n\n## Vue in Markdown\n\nVitePress pages are Vue templates; Blume pages aren't. **In `.mdx` these fail the build**: an HTML comment, `{{ … }}`, a `:prop=` binding, `<script setup>`, an unclosed `<br>`/`<img>`, an `<https://…>` autolink, and `{name}` in prose. **In `.md` they pass through as text or raw HTML** with no error. The codemod reports each.\n\n- **`{{ expr }}`, `$frontmatter`, `$params`** → substitute the value; shared values → Blume `variables`; literal braces → inline code. Vue directives on HTML (`v-if`, `v-for`, `v-pre`) → static content.\n- **`<!-- comments -->`** → `{/* … */}` in `.mdx` (the codemod converts one-line ones).\n- **`<script setup>`, `<script>`, `<style>`** → delete; move styles to a root `theme.css`.\n- **`<Badge type text />`** → `<Badge variant>text</Badge>` (`tip`, the default → `accent`; `info` → `default`; `warning`, `danger` as named; **2.0** `note` → `default`, `important` → `accent`, `caution` → `danger`). Blume leaves a badge's text out of the heading's id (`## Title <Badge>beta</Badge>` → `#title`), as VitePress did for a self-closing badge, but VitePress put a badge's children in it (`<Badge type=\"info\">beta</Badge>` → `#title-beta`), so the codemod pins each converted badge heading to its VitePress id (`[#title-beta]`; `[#title]` for a self-closing badge, redundant but harmless). Unconverted in `.mdx`, a `<Badge type text />` renders empty and warns `BLUME_UNKNOWN_PROP`.\n- **Components** (global ones from the theme's `enhanceApp` or a theme package, and imported `.vue` files): decide per component: a Blume component, Markdown, a static `.astro` component in `components.ts`, or the `.vue` file as an island (`islands/`, with `@astrojs/vue vue` installed; `:users=\"['a']\"` → `users={[\"a\"]}`). Components importing from `vitepress` (`useData`, theme parts) can't run outside it. **First check the old build: a component the theme never registered rendered nothing** (`<!---->` in the HTML), so converting it shows content readers never saw. Report it. Data-driven theme components with no source of data (API lists, contributor avatars) → drop, delete headings left empty, and report the lead-in sentences that now point at nothing.\n- **Raw `<img>`/`<figure>` with a relative `src`** → a Markdown image, which is optimized. Left as is, it works: Blume publishes a file a relative `src` names beside the page, unoptimized, and `blume validate` checks it.\n- **`<ClientOnly>`** → drop the wrapper (an island that needs `window` uses `client: \"only\"`). **`VPTeamMembers`** → a `<CardGroup>` of `<Card>`s, or drop (report).\n- **Data loaders** (`*.data.{js,ts}`) → inline the data, compute it in a custom `.astro` page, or use a content source (report). **Dynamic routes** (`[param].md` + `.paths.*`) → write one page per generated path, then delete the template.\n\n## Home page (`layout: home`)\n\n- **The site home** → `index.mdx` with `mode: custom` (no title or description rendered, so write the heading): the hero as heading, paragraph, and links; each feature as a `<Card title href icon>` in a `<CardGroup cols={3}>` (an image path or inline `<svg>` icon passes to `icon`; an emoji has no slot). For a designed landing page, `pages/index.astro` on `PageLayout`.\n- **Any other page with `layout: home`** (a team or about page using only the hero) → default doc mode: `hero.name` → `title`, `tagline` → a paragraph.\n- Hero CSS variables and theme-package keys (`testimonial`, `features` styling) → drop (report).\n\n## Frontmatter\n\n| VitePress | Blume |\n| --- | --- |\n| `title` | see Titles |\n| `description` | pass through |\n| `head` | `meta name=description` → `description` (when the page has none; VitePress didn't emit a page's `head` description beside a frontmatter one); `og:image` → `seo.image`; `link rel=canonical` → `seo.canonical`; `robots` `noindex` → `noindex: true`; `keywords` → drop; anything else → drop (report) |\n| `layout: doc` | remove |\n| `layout: page` | `mode: frame`. **If the page's only content was a component you're dropping, use the default mode** so the title renders |\n| `layout: home` | see Home page |\n| `layout: false` / a custom layout | `mode: custom`, or a custom `.astro` page (report) |\n| `sidebar: false` | `mode: center` |\n| `aside: false` / `outline: false` | `mode: wide`, or drop; `outline` levels, `aside: left` → drop (report) |\n| `prev: false` **and** `next: false` | `pagination: false`; other `prev`/`next` → drop (report) |\n| `lastUpdated: <date>` | `lastModified`; `false` → remove |\n| `titleTemplate`, `editLink`, `footer`, `navbar`, `pageClass`, `markdownStyles`, theme-package keys | drop (report), or declare with `frontmatter.extend` if a component you wrote reads them |\n\n## Icons\n\nVitePress has no page-icon system. Icons you add (tabs, `meta.ts`, cards) are kebab-case Lucide names; an inline Lucide SVG (`class=\"lucide lucide-server\"`) maps to that name (`server`).\n\n## Assets\n\n- **`<srcDir>/public/`** → `public/` beside `blume.config.ts`; root-absolute references keep working.\n- **Relative images** work as written and are optimized, **except draw.io/diagrams.net SVG exports**: the `content=\"<mxfile…>\"` attribute on their `<svg>` hides the size Astro reads, so Blume serves the file as it is and warns `BLUME_SVG_UNOPTIMIZED` at the line that embeds it. Strip that attribute to have it optimized.\n- **`.html` files under `public/`** (`/pure.html`) → keep the links.\n\n## i18n\n\nA `root` locale with others in `fr/`, `zh/` folders matches Blume's `dir` layout: `i18n: { defaultLocale, locales: [{ code, label }] }`, `code` = the folder (not `lang`), `defaultLocale` = the root's language (`en-US` → `en`). Per-locale nav labels → label maps on `navigation.tabs`/`actions`; per-locale sidebars → each locale's `meta.ts`; per-locale `title`/`description` → drop (report). Pin translated headings to their own old ids: translated pages keep their own old anchors, and the anchor script (see URLs and redirects) pairs each page with its own old page, never with the default locale's. With every locale in a folder (`en/` too), move the default up to the root and redirect `/en/…`, or set `hideDefaultLocalePrefix: false`.\n\n## URLs and redirects\n\nThe old URL list is `<old build>/**/*.html` (or the live `sitemap.xml`). Blume serves `/guide/foo` for `guide/foo.md` and `/guide` for `guide/index.md`.\n\n- **`cleanUrls: false` (the default): one exact redirect per non-index page,** `{ from: \"/guide/foo.html\", to: \"/guide/foo\" }`. `vitepress-codemod.mjs --redirects <old build>` prints them all; paste the array into `redirects`. A pattern can't express a `.html` suffix (`/:path*.html` fails validation).\n- **Don't redirect an index URL** (`/guide/`, `/guide/index.html`, `/index.html`): static hosts serve Blume's `guide/index.html` there already, and `blume dev`/`preview` redirect `/guide/` to `/guide`. `from: \"/guide/\"` takes over the page (`BLUME_REDIRECT_MATCHES_PAGE`); `from: \"/guide/index.html\"` builds (no redirect page is written over the page) but is unneeded.\n- **`cleanUrls: true`:** old URLs already match.\n- **A moved route** (undone `rewrites`, a renamed `_`-prefixed page, a numeric prefix Blume strips, a `foo.md` beside `foo/index.md`, which is one route in Blume: `BLUME_DUPLICATE_ROUTE`) → redirect both the `.html` and the clean spelling.\n- **Internal links:** the codemod rewrites `.html`, absolute `.md`, and trailing-slash links that a page backs; it reports the rest. `blume validate` reports each `.html` link left (`BLUME_BROKEN_ASSET`, naming the page's route when one matches). Fix links in config, frontmatter (`hero.actions`), and raw `<a href>` too.\n- **Anchors.** VitePress slugs headings its own way (punctuation runs → one `-`, leading digit → `_`); Blume uses `github-slugger` (punctuation dropped). On a large site hundreds of ids change (`#what-s-new` → `#whats-new`, `#tsed-core` → `#tsedcore`). After the first build, run the anchor script against the old build: `node <skill>/scripts/pin-heading-ids.mjs --old <old build>` (`--write` to apply), then rebuild and rerun it until it reports 0. By default it pins every id that changed; `--only-linked` pins only the ids content links to, for a site where hundreds change, and is a choice to report, since outside deep links to the rest then land at the top of the page. It reads the ids VitePress published, so a custom `markdown.anchor.slugify` is covered too, and it finds each old page at its own route, so add `--map` only for pages that moved. A `NOTE` names a heading it couldn't pin: pin it by hand (`## What's new [#what-s-new]`) if the heading still exists. A pin **replaces** the generated id, so run `blume validate --strict` after: any `BLUME_BROKEN_ANCHOR` left was already dead on the old site (retarget it or drop the fragment, and report it).\n- **Verify** in `dist/`: every old URL is either a built page (`dist/<route>/index.html`) or a `from` in `dist/_redirects` / `dist/vercel.json` whose target is a built page. Static builds also write a meta-refresh page at `dist/<route>.html/index.html`. `blume preview` serves both, applies the redirects as the host files do, and redirects `/<route>/` to `/<route>`, so you can click through old URLs there. On Netlify, set `deployment: netlify({ output: \"static\" })`: only its `_redirects` forces the rules, so a build for no named host gets the meta-refresh page with a 200. How GitHub Pages serves `/<route>.html` is unverified, so check a few old URLs after deploying there.\n\n## Teardown\n\n- Remove `vitepress`, its plugins (`vitepress-plugin-*`, `@shikijs/vitepress-twoslash`, `markdown-it-*`), the theme package, and `vue` (keep `vue` + `@astrojs/vue` for Vue islands). Delete `.vitepress/` (once its `dist/` is copied out), data loaders, `.paths.*` files, and PostCSS/Tailwind configs that only served the theme. Theme CSS worth keeping → a root `theme.css` against Blume's tokens (`--vp-c-brand-1` → `theme.accent`).\n- **Check the `tsconfig.json` in the Blume project dir**: Blume's build reads it, so drop `.vitepress` includes and make sure `extends` resolves from that dir (install the root workspace first), or delete it if it only served VitePress. An unresolvable `extends` fails the build (`BLUME_TSCONFIG_EXTENDS`, at the line that names it).\n- Repoint `docs:dev`/`docs:build`/`docs:preview` to `blume dev`/`build`/`preview`. Grep the repo for `.vitepress/dist` and `vitepress build`: CI and deploy configs (Pages workflows, publish scripts, `netlify.toml`) must now publish `dist/` and install the docs package's deps. `.gitignore`: `.vitepress/cache`, `.vitepress/dist` → `.blume`, `dist`.\n- A `404.md` → delete (Blume ships a 404; `pages/404.astro` replaces it).\n\n## Dropped — report these\n\n`titleTemplate`, the footer `message` (and any link inside `copyright`), forced `appearance`, per-page `outline`/`aside`, one-sided `prev`/`next`, page-level toggles (`editLink`, `footer`, `navbar`, `pageClass`), `head` entries beyond meta tags and analytics, build hooks, Vite and custom markdown-it plugins, unsupported social links, line-number offsets and the site-wide switch, snippet regions and include ranges (now split files), copied out-of-root snippets, data-driven and unregistered theme components (and any content they hid), `VPTeamMembers`, data loaders and dynamic routes, hero styling, the DocSearch index (with its credentials), old-site anchors that were already broken (and any left unpinned under `--only-linked`), version-dropdown links, and `::: note`/`important`/`caution` text that 1.x showed literally.\n",".agents/skills/blume-migrate/references/vuepress.md":"# VuePress → Blume\n\nVuePress is the Vue static site generator that VitePress succeeded. Its config is a module in `.vuepress/`, navigation is **declared in the theme's `sidebar`**, every `.md` page compiles as a **Vue single-file component**, and its Markdown is markdown-it with `::: name` containers. Most of that layer matches VitePress, so **this file covers only what differs, and sends you to a named section of `references/vitepress.md` for the rest. Keep that file open too.** Watch for six traps:\n\n- **`README.md` is a folder's index page in VuePress, but an ordinary page in Blume.** `guide/README.md` publishes at `/guide/README`, and the folder URL `/guide` has no page. Rename every `README.md` to `index.md` (or `.mdx`), then fix the links to it (see \"URLs and redirects\").\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns only for a callout name in `.mdx`; inside an open `:::` container the line closes it instead. See vitepress.md § \"Markdown extensions\".\n- **URLs end in `.html`** (`/guide/getting-started.html`). Every non-index page needs a redirect.\n- **v1 sidebar groups are collapsible unless `collapsable: false`** (note the spelling). That's the opposite of VitePress and of VuePress 2.\n- **A page's title is its frontmatter `title`, falling back to the body `# H1`, and a page often has both, with different text.** See \"Titles\".\n- **Theme components fail silently.** `<code-group>`, `<code-block>`, and `<router-link>` have hyphenated names, so they pass through as unknown HTML elements, in `.mdx` too. A code group renders every block stacked, and a router link renders with no `href`. Neither `build` nor `validate` reports them.\n\nScope: **VuePress 1** (`vuepress@1.x`, Vue 2, webpack) with the default theme or a theme that `extend`s it, in full. **VuePress 2** (`vuepress@2.0.0-rc.*`, Vue 3, `@vuepress/theme-default` 2.x) is covered in notes marked **v2**. `vuepress-theme-hope` (v2) has its common syntax under \"Themes\". For any other theme (`vuepress-theme-reco`, `vuepress-theme-plume`, an ejected `.vuepress/theme/`), inventory its components and frontmatter by hand. The v1 mappings that VuePress's own docs exercise were run end to end on them; the rest of v1 (`permalink`, `base`, `vuepress-plugin-tabs`, the blog plugin, …), the **v2** notes, and the hope section come from source and docs and haven't been run.\n\n## Detect\n\n- **A `.vuepress/` directory** holding `config.{js,ts}`, or v1's `config.yml`/`config.toml`, or v2's `config.mjs`. It's usually `docs/.vuepress/`. **v2** also reads `vuepress.config.{ts,js,mjs}` from the directory the CLI runs in. The directory that holds `.vuepress/` is the **source dir**: the argument to `vuepress dev <dir>` in the `package.json` scripts.\n- **Dependencies:** `vuepress` (both majors), the v2 betas' `vuepress-vite`/`vuepress-webpack`, or `@vuepress/*`, `vuepress-plugin-*`, and `vuepress-theme-*` packages. They're often only in the docs package's own `package.json`.\n- **Which major:** a `vuepress` version of `^1` means v1. A `2.0.0-*` version, `defineUserConfig`, `bundler: viteBundler()`, or `theme: defaultTheme({…})` means v2. A v1 config is CommonJS (`module.exports`) or `defineConfig` from `vuepress/config` (1.9+), with the theme's options under `themeConfig`. A function config (`ctx => ({…})`) may branch on `ctx.isProd`, so read both sides.\n- **Not VitePress.** VitePress uses `.vitepress/` and a `vitepress` dependency (`references/vitepress.md`).\n\n**Where the Blume project goes:** `blume.config.ts` goes in the directory the docs scripts run in (the `package.json` holding `vuepress build docs`, which may be a workspace package like `packages/docs`, not the repo root), with `content.root` set to the source dir (the default `docs` usually matches). When the scripts run `vuepress build` inside the source dir itself, use vitepress.md § \"Detect\"'s second layout. The public directory is `<source dir>/.vuepress/public` (in **v2**, the `public` option). Move it to `public/` beside `blume.config.ts`. Blume's default `exclude` (`**/.*`) already skips `.vuepress/`.\n\n## The VitePress codemod\n\nvitepress.md's codemod (`<skill>/scripts/vitepress-codemod.mjs`) also reads VuePress 1 pages. Over VuePress's own docs, with `README.md` already renamed to `index.md`, it converted containers, `details`, `v-pre`, comments, `<Badge>`s (pinning their heading anchors to the old ids, which Blume now gives them anyway), `[[toc]]`, glued fence ranges, body H1s, and links that a page backs, and listed much of the rest as `REVIEW` lines. Run it on the source dir **after** the `README.md` renames (before them, links to folder indexes come back as unbacked; it leaves `.vuepress/` alone), then correct what it treats as VitePress:\n\n- **Not reported at all:** lowercase and hyphenated tags (`<code-group>`, `<code-block>`, `<router-link>`, `<demo-1>`; `<Foo-Bar/>` is reported as `<Foo>`), `:emoji:` shortcodes, `@flowstart` blocks, `sidebar: auto` (which then fails the build), **v2** `@[code]` imports, and relative `README.md` links, which stay as written. Grep for each, and rewrite the `README.md` links to the folder (see \"URLs and redirects\").\n- **`<code-group>` and `<code-block>`** aren't converted, and a page with nothing else to convert stays `.md` (VuePress's English `guide/getting-started.md` did). Convert them, and rename the page to `.mdx`. **v2** `::: code-tabs` / `::: tabs` come back as unknown containers to convert by hand.\n- **The v2 betas' `:::: code-group` + `::: code-group-item Title`** is read as VitePress's `::: code-group`: it comes out as an empty `<CodeGroup>` followed by the items, with a removed \"stray\" closer. Convert those blocks by hand before running it.\n- **v1 badge types** `error`, `warn`, and `yellow` come out as `accent`: map them as \"Vue in Markdown\" says.\n- **A frontmatter `title` that differs from the H1** is reported for `seo.title`. In VuePress it was the sidebar text, so it goes to `sidebar.label` (see \"Titles\").\n- **`<<< @/…`** resolves `@` against the source dir, which is wrong when `vuepress` ran elsewhere. Check each import it converts or reports against the `<<<` row below.\n- **`--redirects`** reads only VitePress builds: on a VuePress build it prints `[]`, which doesn't mean no redirects are needed. Use the command in \"URLs and redirects\".\n\n## Config: `.vuepress/config.*` → `blume.config.ts`\n\nRead the whole config, including the modules it imports. VuePress sites often keep `nav` and `sidebar` in per-locale files under `.vuepress/config/`.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `head` | same as VitePress `head` (vitepress.md § \"Config\"); the `[tag, attrs, content]` format is identical. `link rel=\"apple-touch-icon\"` → copy the file to `public/apple-touch-icon.png`, which Blume detects. `manifest` and `mask-icon` links → drop with the PWA |\n| **v2** `lang` | same as VitePress `lang` |\n| `locales` | `i18n` (see \"i18n\") |\n| `dest`, `temp`, `cache`, `host`, `port`, `open`, `shouldPrefetch`, `shouldPreload`, v1 `devTemplate` / `ssrTemplate`, **v2** `templateDev` / `templateBuild`, `evergreen`, `extraWatchFiles`, `debug` | drop |\n| `patterns` / **v2** `pagePatterns` | `content.include` / `exclude`. v1's default pattern also builds `.vue` files as pages: rebuild each as Markdown or a custom `.astro` page (report) |\n| `permalink` (v1, site-wide) / **v2** `permalinkPattern` | no setting. Give each affected page a `slug` that reproduces its old URL (see \"URLs and redirects\") |\n| **v2** `public` | the directory to move to `public/` |\n| `theme` | see \"Themes\" |\n| `markdown.lineNumbers` (v1) | no site-wide switch. Add `lineNumbers` to the fences that need it, or drop it (report) |\n| `markdown.slugify` | a custom slugify changes every anchor. See vitepress.md § \"URLs and redirects\" |\n| `markdown.anchor`, `markdown.toc`, `markdown.pageSuffix`, `markdown.extractHeaders` / **v2** `markdown.headers` | drop. If `extractHeaders` added `h4`, set `toc: { maxHeadingLevel: 4 }` |\n| `markdown.externalLinks` / **v2** `markdown.links.externalAttrs` | VuePress opens external links in a new tab by default. To keep that, set `markdown.externalLinks: true` (optional) |\n| `markdown.extendMarkdown`, `chainMarkdown`, `markdown.plugins` / **v2** `extendsMarkdown` (also inside local plugins) | custom markdown-it plugins. Find the syntax each adds and convert every use, as in vitepress.md's `markdown.config` row. Report each plugin |\n| **v2** `markdown.importCode.handleImportPath` | aliases for `@[code]` paths. Apply them as you convert those imports |\n| `configureWebpack`, `chainWebpack`, `postcss`, `stylus`, `scss`, `sass`, `less`, `alias`, `define`, **v2** `bundler` | drop (report) |\n\n**Theme options** (v1 `themeConfig`; **v2** the `defaultTheme({…})` options):\n\n| VuePress | Blume |\n| --- | --- |\n| `logo` | `logo`. Move the file into `public/`. **v2** `logoDark` → `logo: { image: { light, dark } }`, and `logoAlt` → `alt` |\n| `nav` / **v2** `navbar` | see \"Navigation: `nav`\". `navbar: false` → drop (report) |\n| `sidebar` | see \"Navigation: `sidebar`\" |\n| `sidebarDepth`, `displayAllHeaders`, `activeHeaderLinks` | drop. VuePress listed a page's headings in the sidebar; Blume lists them in the outline rail (`h2`–`h3`, set by `toc`) |\n| `repo` (`owner/name` means GitHub) | `github` adds the footer mark **and** an Edit on GitHub action, so set it only where the site had edit links: v1 with `editLinks: true`, **v2** unless `editLink: false`. Otherwise keep just the mark: `navigation.repo: \"https://github.com/<owner>/<name>\"`. A GitLab or Bitbucket URL → a `navigation.actions` link; edit links drop (report) |\n| `docsRepo` / `docsBranch` / `docsDir` (edit links) | `github: { owner, repo, branch, dir }`, from `docsRepo` when it's set (then point `navigation.repo` at `repo`). `docsBranch` defaults to `master` in v1 and `main` in **v2**, so set `branch` to the real branch. `docsDir` is the repo path to the source dir, so `github.dir` is `docsDir` minus `content.root` (`packages/docs/docs` → `packages/docs`). **v2** `editLinkPattern` → read the path from it the same way |\n| `lastUpdated` | v1 turns the date on when `lastUpdated` (a label or `true`) is set in `themeConfig` **or in any `themeConfig.locales[path]`**: either → `lastModified: \"git\"`, and the label drops. **v2** shows it unless `lastUpdated: false`; map it only when the config sets it, and report the default otherwise |\n| `nextLinks: false` **and** `prevLinks: false` (v1) | `pagination: false` on every page: there's no site-wide switch. Only one of them → drop (report) |\n| `search: false` | `search: false` |\n| `algolia` (`apiKey`, `indexName`, `appId`), in `themeConfig` or, in v1, any `themeConfig.locales[path]` | omit `search` (Orama) and report the credentials. A v1 `algolia` block is almost always **DocSearch**: Algolia's crawler fills the index (the app is `BH4D9OD16A` when `appId` is missing, and `algoliaOptions.facetFilters` often marks a shared index). Its records aren't Blume's, and Blume's sync, given an admin key, replaces the whole index on every build. Never point `algolia()` at it; a new index of your own is a separate opt-in |\n| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` → report: Blume always shows the toggle |\n| **v2** `hostname` | `deployment.site`, unless the target host is Vercel or Netlify |\n| `locales` | see \"i18n\" |\n| `smoothScroll`, `searchMaxSuggestions`, `searchPlaceholder`, `repoLabel`, `editLinkText`, **v2** `contributors`, `externalLinkIcon`, `home`, `notFound`, `backToHome`, and the other UI labels | drop. A translated UI string → `i18n.ui` |\n\n**Plugins** (`plugins`, plus the ones the theme turns on):\n\n| VuePress | Blume |\n| --- | --- |\n| `@vuepress/plugin-search` (v1's built-in search), **v2** `search`, `slimsearch`, `flexsearch`, `orama` | omit: Orama is the default |\n| **v2** `@vuepress/plugin-meilisearch` | omit, and report: Blume has no Meilisearch adapter |\n| **v2** `@vuepress/plugin-docsearch` | as the `algolia` row: omit `search` and report |\n| `@vuepress/plugin-google-analytics` (v1 `{ ga }`, **v2** `{ id }`) | `analytics: [googleAnalytics({ id })]` from `blume/analytics`. A `UA-` id stopped collecting when Google retired Universal Analytics, so ask for the GA4 `G-` id, or drop it (report) |\n| Other analytics plugins | the matching `blume/analytics` adapter, or `script()` |\n| `vuepress-plugin-sitemap`, **v2** `@vuepress/plugin-sitemap` / `plugin-seo` (`hostname`) | `deployment.site` (with the same host rule as above). Blume writes the sitemap, canonicals, and Open Graph tags itself |\n| `@vuepress/plugin-medium-zoom` | omit: Blume content images zoom on click by default |\n| `@vuepress/plugin-back-to-top` | omit: Blume's page actions include \"Scroll to top\" |\n| `@vuepress/plugin-pwa` | drop (report). Blume has no service worker. The `manifest` `head` link drops with it |\n| `@vuepress/plugin-last-updated` | `lastModified: \"git\"` |\n| `nprogress`, `active-header-links`, `smooth-scroll`, **v2** `copy-code`, `git` | drop |\n| `@vuepress/plugin-register-components` | see \"Vue in Markdown\" |\n| `container` (`vuepress-plugin-container`) / **v2** `@vuepress/plugin-markdown-container` | containers the site registers. Read each entry's `type`, `before`, `after`, and `defaultTitle`, then map it as in vitepress.md's last \"Markdown extensions\" row (report) |\n| **v2** `@vuepress/plugin-redirect` | its `config` map → `redirects`. `redirectFrom` frontmatter → one `redirects` entry per old path, pointing at that page. A page with `redirectTo` → delete it and add a redirect |\n| `@vuepress/plugin-blog`, `vuepress-plugin-blog` | `type: blog` pages plus a hand-written index (see `docs/advanced/blog.mdx`). Tag, category, and pagination pages → drop (report) |\n| Mermaid, math, tabs, flowchart, and include plugins | see \"Markdown extensions\" |\n\n**Styles:** v1's `.vuepress/styles/palette.styl` `$accentColor` (or **v2** `--vp-c-accent` in `.vuepress/styles/index.scss`) → `theme.accent`. Move other rules worth keeping from `index.styl` / `index.scss` to a root `theme.css`, written against Blume's tokens (report).\n\n## Navigation: `sidebar`\n\nThe rules in vitepress.md § \"Navigation: `sidebar`\" apply: multiple sidebars become tabs, a group whose pages sit flat beside other groups' pages becomes a **`(group)` folder** (`guide/(advanced)/frontmatter.md` still serves `/guide/frontmatter`, so no redirects; list it in the parent `meta.ts` `pages` by its bare name, `advanced`), and pages no sidebar lists need a decision. The item formats differ:\n\n- **Multiple sidebars** (`sidebar: { '/guide/': [...], '/': [...] }`, fallback key last) → one `navigation.tabs` entry per non-root key, as in VitePress. Items inside a key are relative to it: `''` is the key's `README.md`, and `'getting-started'` is `/guide/getting-started.html`.\n- **Items** are page paths, with `.md` optional, or `[path, 'Text']`. A `Text` that differs from the page's title → that page's `sidebar.label`.\n- **v1 groups** (`{ title, path, collapsable, sidebarDepth, children, initialOpenGroupIndex }`) → a folder (or `(group)` folder) with a `meta.ts` (`title`, `pages` in order). `path` → the folder's `index` page. `collapsable` unset or `true` → `display: \"group\"`. Blume's groups then start collapsed except the one holding the current page, as in v1 (which also opened the first group, or `initialOpenGroupIndex`, on a page no group held). When every group collapses, set `navigation.sidebar.display: \"group\"` once. `collapsable: false` → keep the default `flat`. `sidebarDepth` and `initialOpenGroupIndex` → drop.\n- **v2 groups** (`{ text, link, prefix, collapsible, children }`, where strings are file paths such as `'foo.md'` or `'/bar/README.md'`, resolved against `prefix`) → the same, except `collapsible: true` is the only collapsible case.\n- **`'auto'`** (v1, as a key's value or the whole `sidebar`) and **v2** `'heading'` (the default) showed only the current page's headings. Blume shows the page tree instead, with the headings in the outline rail. Report that the sidebar changed.\n- **Pages reachable only from the navbar** (common: `config/`, `faq/`, `miscellaneous/` with `sidebar: auto`) → give each section a place in the tree: a tab for a top-level navbar link, or a folder for a dropdown group. Use `sidebar.hidden: true` only for pages that were truly unlisted.\n\n## Navigation: `nav`\n\nMap it as in vitepress.md § \"Navigation: `nav`\". v1 items are `{ text, link }`, or `{ text, items }` for a dropdown; a dropdown can hold `{ text, items }` subgroups, which flatten. **v2** items are `{ text, link }`, `{ text, children, prefix }`, or a page path string. Strip `.md` and `.html` from internal `link`s, and turn `README.md` into its folder. `ariaLabel`, `target`, `rel`, and `activeMatch` → drop.\n\n**A dropdown whose pages span several folders** (VuePress's own \"Learn More\": `api/`, `faq/`, `miscellaneous/`, and an external changelog) fits no single tab. Make it a tab with `path: \"/\"` and `items` for the pages (on those routes the sidebar lists the folders no other tab claims, which here are exactly those, though the tab also highlights on the home page), and move its external links to `navigation.actions`. Or, for a few pages, use `navigation.featured`.\n\n## Titles\n\nVuePress names a page with its frontmatter `title`, falling back to a heading on the page's first line (normally the `# H1`). That name is the sidebar text and the `<title>`, while the H1 still renders as written. v1's `metaTitle` overrides the whole `<title>`. Blume renders frontmatter `title` as the H1. On every page:\n\n1. Move the H1 text into `title` and delete the H1 line. Keep only the text of an H1 that carries a link or a `<Badge>`: move the link into the body, and the badge to `sidebar.badge`.\n2. A frontmatter `title` that differs from the H1 → `sidebar.label`, since it was the sidebar text.\n3. `metaTitle` → `seo.title`, minus VuePress's ` | Site` suffix: Blume adds ` - <site title>` itself. A `metaTitle` without that suffix was the whole `<title>`: keep it as written and report that the site name is now appended.\n\nA page with no H1 and no `title` took the site title. Give it a real one. **VuePress gave the H1 an id, but Blume's title has none**, so a link to it (`using-a-plugin.html#using-a-plugin`, meaning the top of the page) fails `validate` with `BLUME_BROKEN_ANCHOR`: drop the fragment.\n\n## Markdown extensions\n\nEverything Blume renders from these is MDX-only unless vitepress.md marks it `.md` OK, so a page that needs a callout or component is renamed to `.mdx`.\n\n| VuePress | Blume |\n| --- | --- |\n| `::: tip` / `::: warning` / `::: danger` (v1 default theme). **v2** adds `info`, `note`, `important`, and `caution` | same as VitePress's containers, with the space removed and a title moved into `[Title]`. **v2** `important` → `:::note` (Blume's alias), `caution` → `:::danger`. v1 has no `note` or `info` container: unless a plugin registers one, the live site showed those lines as text, so converting them changes the page (report) |\n| `::: details Title` | same as VitePress: `<Expandable title=\"Title\">`, or `title=\"Details\"` when untitled. VuePress has no `{open}` |\n| `::: v-pre` | same as VitePress's `::: raw` row |\n| `::: slot name` (v1 Markdown slots) | no equivalent. Put the content where the layout showed it, or drop it (a home page's `::: slot footer` → see \"Home page\") (report) |\n| `<code-group>` + `<code-block title=\"YARN\">` (v1, 1.6+) | `<CodeGroup>` with blank lines inside, each block's `title` becoming its fence's title (` ```bash YARN `, or `title=\"Two words\"`). `active` on a later block → move that block first (Blume opens the first tab), or use `<Tabs defaultTabIndex={n}>` with one fence per `<Tab title>`. Inside a list item, indent `<CodeGroup>`, its fences, and the closing tag to the item's content column, or the list breaks. Unconverted, the blocks render stacked with no warning |\n| `<CodeGroup>` + `<CodeGroupItem title>`, or `:::: code-group` + `::: code-group-item Title` (**v2** betas) | the same. Unconverted in `.mdx`, `CodeGroupItem` fails the build, and the spaced containers show as text |\n| `::: code-tabs#id` + `@tab Label` / `@tab:active Label` (**v2** default theme, hope) | `<CodeGroup>`, each `@tab` label becoming the title of the fence that follows it (VuePress rendered only that first fence). `#id` → drop: Blume already syncs same-titled tabs across the page. `:active` → as for `<code-block active>` |\n| `::: tabs#id` + `@tab Label` (**v2**, hope) | `<Tabs>` of `<Tab title=\"Label\">`. `#id` → `syncKey=\"id\"`, `@tab:active` → `defaultTabIndex={n}` (zero-based), and `@tab Label#value` → keep only `Label` |\n| `:::: tabs` + `::: tab \"Label\"` (`vuepress-plugin-tabs`, v1) | `<Tabs>` of `<Tab title=\"Label\">` |\n| ` ```js{4} ` | same as VitePress: Blume reads it as written, and the codemod adds the space |\n| **v2** ` ```ts title=\"config.ts\" ` | works as written (`.md` OK) |\n| **v2** `:line-numbers`, `:line-numbers=N`, `:no-line-numbers` | the first two as in VitePress; `:no-line-numbers` → remove. **v2** turns line numbers on for every block by default; Blume has no site-wide switch (report). `:collapsed-lines`, `:v-pre`, `:no-v-pre` → remove |\n| **v2** `// [!code …]` (the `notation*` options) | same as VitePress |\n| `<<< @/path/file.js{2}` / `<<< @/file.js#region` (v1) | `<include>`, mapped as in VitePress's `<<<` rows, but **`@` is the directory `vuepress` ran in, not the source dir.** Running `vuepress build docs` from the repo root makes `@/docs/x.js` → `<include>/x.js</include>`. v1 imports take no `[label]` or `{lang}`. A region, or a file outside the content root → split or copy the file, as VitePress's rows say |\n| **v2** `@[code](./file.js)` / `@[code{3-10} js{2}:no-line-numbers](../file.js)` | `<include lang=\"js\" meta=\"{2}\">../file.js</include>`. Paths are relative to the page, like `<include>`'s. A line range (`{3-10}`) → split the file (report). Unconverted, it renders as `@` plus a link to the file, with no error |\n| **v2** `<!-- @include: ./part.md -->`, with `{start-end}` or `#region` (`@vuepress/plugin-markdown-include`, md-enhance, hope) | same as VitePress's `<!--@include:-->` row. Unconverted in `.md` it stays an HTML comment, so the content disappears silently. In `.mdx`, it fails the build |\n| `[[toc]]`, `:emoji:` | same as VitePress. VuePress left an emoji shortcode out of a heading's anchor (`## Emoji :tada:` → `#emoji`), and Blume's `## Emoji 🎉` is `#emoji` too, so the pasted emoji keeps the id |\n| `@flowstart` … `@flowend` (`vuepress-plugin-flowchart`) | renders as literal text. Redraw it as a ` ```mermaid ` flowchart (`.mdx`) or an image (report) |\n| **v2** `> [!NOTE]` GitHub alerts (with the hint plugin's `alert` on, or in hope) | same as VitePress's alerts row |\n| Math, ` ```mermaid `, markdown-it-attrs (`{#id}`, `{.class}`) | same as VitePress's rows. v1 and v2 core have no `{#id}` or attrs; only plugins (hope's `attrs`) add them |\n\n## Vue in Markdown\n\nvitepress.md § \"Vue in Markdown\" applies: the `.mdx` build errors, `{{ }}`, `<script>`/`<style>` blocks, comments, `<ClientOnly>`, directives, and the per-component decision. VuePress adds:\n\n- **Globals:** `$page`, `$site`, `$frontmatter`, `$themeConfig`, `$lang`, `$localePath`, `$title`, `$description`, and `$style` (from `<style module>`) → substitute or delete. `$withBase('/img.png')` → the plain root path (`![alt](/img.png)`): Blume adds `deployment.base` to internal links and assets.\n- **`<Badge>`** → Blume `<Badge>`, with the text as **children**. v1 `type`: `tip` (the default) or `green` → `accent`; `warning`, `warn`, or `yellow` → `warning`; `error` → `danger`. **v2** `type`s map as in vitepress.md. `vertical` → drop. An unconverted badge renders empty in `.mdx`, which warns `BLUME_UNKNOWN_PROP`, and as nothing in `.md`, with no warning. **Anchors:** VuePress left a badge out of a heading's anchor (`## Custom Containers <Badge text=\"default theme\"/>` → `#custom-containers`), and so does Blume, so the converted heading keeps its id.\n- **`<router-link to=\"/x.html\">`** (v1), `<RouterLink>`, and **v2** `<RouteLink>` → a Markdown link to the route (`[text](/x)`). `<OutboundLink/>` and `<Content/>` → delete.\n- **Global components:** v1 registers every `.vuepress/components/**/*.vue` file globally, named after the path (`Foo/Bar.vue` → `<Foo-Bar/>`, `demo-1.vue` → `<demo-1/>`), plus theme `global-components` and `Vue.component()` calls in `.vuepress/enhanceApp.js`. **v2** registers them through `@vuepress/plugin-register-components` or `.vuepress/client.{js,ts}`. Decide each as in VitePress. A hyphenated tag passes through as an unknown HTML element, so give the Blume component a PascalCase name and rename its tags (`<demo-1/>` → `<Demo1 />`). **v1 components are Vue 2**, but `@astrojs/vue` runs Vue 3, so an island needs a Vue 3 port: functional components, filters, `$listeners`, `.sync`, and `this.$set` all changed (report). A component that reads `$page`, `$site`, `$themeConfig`, or `this.$router` can't run outside VuePress: rewrite it or drop it.\n- **An escaped `\\<App>`** (VuePress's `api/node` headings) renders as text in `.mdx`, with no warning, so it can stay as written.\n- **`.vuepress/enhanceApp.js` / v2 `client.{js,ts}`** (Vue plugins, router hooks, global components) → drop, and report what each did. **v2** client-config `layouts` → custom `.astro` pages.\n\n## Home page (`home: true`)\n\nRebuild it as in vitepress.md § \"Home page (`layout: home`)\": `index.mdx` with `mode: custom`, or a designed `pages/index.astro`. It's usually the source dir's `README.md` (or `index.md`). VuePress's keys:\n\n- v1: `heroImage`, `heroAlt`, `heroText` (it defaults to the site title, and `null` hides it), `tagline` (it defaults to the site description, and `null` hides it), `actionText` + `actionLink` (one button), `features: [{ title, details }]`, and `footer`. **v2:** `actions: [{ text, link, type }]` replaces the single action, and adds `heroImageDark`, `heroHeight`, and `footerHtml` (which renders `footer` as HTML).\n- `mode: custom` is full-bleed with no padding, so a hero image renders huge and flush left. Wrap the hero in a centered block (`<div style={{ maxWidth: \"60rem\", margin: \"0 auto\", padding: \"3rem 1.5rem 0\", textAlign: \"center\" }}>`, with blank lines inside so the Markdown renders) and cap the image (`style={{ maxHeight: \"280px\", margin: \"0 auto\" }}`). Add `sidebar.hidden: true` so the home page isn't a sidebar row.\n- Features have no icon or link: each becomes a `<Card title>` with `details` as the body, in a `<CardGroup cols={3}>`. Features written as raw HTML in the body (`<div class=\"features\">`) map the same way.\n- `footer` (HTML with **v2** `footerHtml: true`) or a `::: slot footer` is usually a copyright or license line (\"MIT Licensed | Copyright © 2018-present …\") → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); move its links into `footer.links`, or they lose their URLs (report). A translated home page's `footer` → that locale's entry in a locale map. Other footer content → drop (report). `heroImageDark` → Blume has no light/dark pair for a content image, so keep one (report).\n\n## Frontmatter\n\nBlume's schema is strict: every key below must be mapped or removed.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `metaTitle` | see \"Titles\" |\n| `description` | pass through |\n| `meta` (v1) / **v2** `head` | per entry, as in vitepress.md's frontmatter `head` row |\n| `canonicalUrl` (v1.7.1+) | `seo.canonical` |\n| `lang` | remove: Blume sets `<html lang>` per locale |\n| `permalink` | `slug`, the old path without its leading `/`, trailing `/`, or `.html` (see \"URLs and redirects\") |\n| `layout` naming a `.vuepress/components` or theme component | `mode: custom`, and rebuild the content, or use a custom `.astro` page (report). `layout: Layout` → remove |\n| `home` and the hero keys | see \"Home page\" |\n| `sidebar: auto` / **v2** `sidebar: heading` | remove: the outline rail lists the headings |\n| `sidebar: false` | `mode: center` |\n| **v2** `sidebar` as an array (a per-page sidebar) | remove (report) |\n| `sidebarDepth` | remove |\n| `prev` / `next` | same as VitePress |\n| `search: false` (v1) | remove (report): it hid the search box on that page, which stayed searchable |\n| `tags` (v1, extra search terms) | `search.keywords` |\n| **v2** `date` | keep on `type: blog` pages, otherwise remove |\n| **v2** `redirectFrom` / `redirectTo` | `redirects` (see the redirect plugin row) |\n| `navbar`, `pageClass`, `editLink`, **v2** `editLinkPattern`, `lastUpdated`, `contributors`, `externalLinkIcon`, `permalinkPattern`, `routeMeta` | drop (report) |\n| keys a theme reads | see \"Themes\" |\n\n## Themes\n\n- **A theme that `extend`s `@vuepress/theme-default`** (v1's `@vuepress/theme-vue`, or a local `.vuepress/theme/index.js` with `extend`) → treat it as the default theme. Layouts it overrides (ads, banners) → drop (report).\n- **An ejected or local theme** (a full `.vuepress/theme/` with `Layout.vue`, `Navbar.vue`, …) → map the `themeConfig` keys it shares with the default theme. Any extra key (a version list inside the sidebar, say) is custom, so read its components to see what it rendered (report).\n- **`vuepress-theme-hope`** (`hopeTheme({…})`, often in `.vuepress/theme.ts`):\n  - **Sidebar** `\"structure\"` builds the tree from files, which Blume does by default. `order` → `sidebar.order`, `index: false` → `sidebar.hidden: true`, and `shortTitle` → `sidebar.label`. Hope sorts positive `order`s first, then unordered pages by title, then negative `order`s (`-1` last), while Blume sorts a negative order first: renumber, or list the folder's order in its `meta.ts` `pages`. A folder `README.md`'s `dir` → that folder's `meta.ts`: `text` → `title`, `icon`, `order`, `collapsible` (on by default) → `display: \"group\"`, and `expanded: true` → `collapsed: false`. `dir.index: false` (the folder left out of the generated sidebar) → `sidebar.hidden: true` on each of its pages.\n  - **Icons** (frontmatter, navbar, sidebar) are FontAwesome or Iconify names → Lucide (SKILL.md \"Icons are Lucide\").\n  - **Markdown** (the theme's `markdown` options, or `plugins.mdEnhance`): `^sup^`, `~sub~`, task lists, and footnotes work as written. `==mark==` → `<mark>…</mark>`. `::: center` / `::: right` → drop the alignment. `!!spoiler!!` → drop. `<VPCard>` → `<Card>`. A ` ```component Name ` fence (YAML or JSON props) → that component with the props as attributes. `<Catalog />` → `directory: \"card\"` in that folder's `meta.ts`. `<!-- more -->` → delete. Charts (Chart.js, ECharts, Markmap, PlantUML, flowchart), playgrounds, `::: demo`, and slides → no equivalent: a mermaid diagram, an image, or a link (report).\n  - **Frontmatter:** `author` and `date` → `authors` and `date` on `type: blog` pages, otherwise drop. `category` and `tag` → drop (report: Blume has no tag pages), or `search.keywords`. `article`, `star`, `sticky`, `cover`, `isOriginal`, `pageInfo`, `breadcrumb`, `toc`, `comment`, `copy`, `footer`, `copyright`, `containerClass`, and any other hope-only key (`license`, `pageview`, `backToTop`, `sitemap`, `feed`, …) → drop. The project home's `highlights` → more cards. `bgImage` and the hero style keys → drop (report).\n\n## Icons\n\nThe default theme has no page icons. Icons you add to tabs, `meta.ts`, cards, or `sidebar.icon` are kebab-case Lucide names. Hope's icons map as above.\n\n## Assets\n\n- **`.vuepress/public/`** → `public/`. Root-absolute references (`/hero.png`) keep working.\n- **Relative images** (`./img.png`) work as written.\n- **Webpack paths** (v1 `~@alias/img.png`, `~some-package/img.png`, the `@source` alias) → copy the file into the content root or `public/` and link it plainly.\n\n## i18n\n\nMap it as in vitepress.md § \"i18n\". VuePress's `locales` keys are paths (`'/zh/'`), so the `code` is the folder (`zh`), not the `lang` (`zh-CN`). The default locale's `lang` (`en-US`) gives `defaultLocale` (`en`). The theme's per-locale options (v1 `themeConfig.locales`, **v2** the theme's `locales`) carry the rest:\n\n- `label` (**v2** `selectLanguageName`) → that locale's `label`. `selectText` (**v2** `selectLanguageText`), `ariaLabel`, `editLinkText`, the `lastUpdated` text, `serviceWorker`, and the other UI labels → drop, or `i18n.ui`.\n- `nav` → per-locale label maps on `navigation.tabs` / `actions`.\n- `sidebar` (keyed `'/zh/guide/'`, …) → each locale's own folder `meta.ts` files.\n- A per-locale container title (`defaultTitle: { '/zh/': '提示' }`) → drop: Blume's UI packs (including `zh`) title callouts.\n- A per-locale `title` / `description` → drop; a translated home page can keep its description in frontmatter.\n- **Anchors:** keep each locale's own old ids (`/zh/` pages had Chinese slugs, which zh pages and outside zh links use). The anchor script does this, pairing every page with its own old page. vitepress.md's \"Pin translated headings\" means exactly that, not pinning to the default locale's ids.\n\n## URLs and redirects\n\nFollow vitepress.md § \"URLs and redirects\", treating VuePress like `cleanUrls: false`: one exact redirect per non-index `.html` page, never a redirect from an index URL, both spellings for a moved route, and rewritten internal links. VuePress specifics:\n\n- **Build the old site first and keep its output** (`<source>/.vuepress/dist`, or `dest`): it's the URL list and the record of the old anchors. VuePress writes no sitemap unless a plugin adds one. v1 runs on webpack 4, which fails on current Node with `error:0308010C:digital envelope routines::unsupported`: build it with `NODE_OPTIONS=--openssl-legacy-provider`, or under Node 16 (`npx -p node@16 …`).\n- **Redirects:** run this in that output directory and paste the result into `redirects` (the VitePress codemod's `--redirects` reads only VitePress builds):\n\n  ```bash\n  find . -name '*.html' ! -name index.html ! -name 404.html | sed 's#^\\.##' | sort \\\n    | awk '{ to = $0; sub(/\\.html$/, \"\", to); printf \"  { from: \\\"%s\\\", to: \\\"%s\\\" },\\n\", $0, to }'\n  ```\n\n- **`README.md` → `index.md`** keeps the folder URL (`/guide/` is served from `guide/index.html`), so it needs no redirect. Rewrite each link to a `README.md` to its folder (`./README.md` → `./`, `/guide/README.md` → `/guide`). Once the file is renamed, `blume validate` flags the ones you miss.\n- **`permalink` pages** served the permalink, not the file path. Set `slug` to it. A permalink ending in `.html` also needs a redirect from that `.html` URL.\n- **Links:** VuePress authors link to `.md` files, relative or absolute (`/guide/foo.md`; **v2** recommends relative), and to `.html`. Rewrite them as vitepress.md says. Also fix links in `nav`, `sidebar`, `actionLink`, hero `actions`, and raw `<a href>`.\n- **Anchors: pin them with the bundled script.** VuePress slugs headings as VitePress does, so ids change on many pages (115 headings on 32 of 92 pages in VuePress's own docs, counted before Blume left badges and trailing emoji out of its ids). **Duplicate headings number differently:** VuePress's second `## Options` is `#options-2`, Blume's is `#options-1`. After `blume build`, run from the Blume project:\n\n  ```bash\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist>          # dry run\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist> --write  # apply\n  ```\n\n  It pairs each page's headings with the old build's by text and position, translated pages with their own old pages, and appends `[#old-id]` (`{#old-id}` in `.md`) where the ids differ. Rebuild and rerun until it reports 0. A `NOTE` names a heading whose source line it couldn't match, such as one with inline code, a Markdown escape (`\\<`), or `--` (19 of the 115 in VuePress's docs): pin those by hand. It also pins the home hero's `#main-title`, which is harmless. On a large site, `--only-linked` pins only the ids something links to.\n\n- **`validate` also reports anchors that were already broken.** VuePress never checked fragments (a zh page linking an English id, a renamed section). Point each at the right heading or drop the fragment, and report it.\n- **Hosts:** Netlify (with pretty URLs) and GitHub Pages also answered `/guide/foo` without `.html`, which Blume keeps. On Netlify, a static build for no named host writes unforced `_redirects` rules, so Netlify serves the meta-refresh page instead of a 301: use `deployment: netlify({ output: \"static\" })` (see Redirects in `docs/02-deployment.mdx`). The GitHub Pages caveat in vitepress.md applies.\n\n## Teardown\n\n- Remove `vuepress`, `vuepress-vite` / `vuepress-webpack`, `@vuepress/*`, `vuepress-plugin-*`, `vuepress-theme-*`, `vue`, `vue-template-compiler`, `vue-server-renderer`, and any markdown-it, Stylus, Sass, or webpack packages that only served VuePress. Keep `vue` with `@astrojs/vue` if you made Vue islands.\n- Delete `.vuepress/` (config, `config/`, `components/`, `theme/`, `styles/`, `enhanceApp.js` / `client.*`, `dist/`, `.temp/`, `.cache/`) once `public/` has moved.\n- Repoint the scripts (`docs:dev`, `docs:build`) to `blume dev` / `blume build` / `blume preview`, and add `blume`. Remove `NODE_OPTIONS=--openssl-legacy-provider`.\n- **Node:** Blume needs 22.19 or later, and v1 sites often pin an old one (`.nvmrc`, `engines`, CI `node-version: '14'`, Netlify's `NODE_VERSION`). Bump each. When the same CI job also builds or tests other packages (yarn 1 refuses to install a dependency whose `engines` excludes the running Node), the bump reaches them too, so report it.\n- Grep the repo for `.vuepress/dist`, `dest`, and `vuepress build`. Deploy scripts (the VuePress guide's `deploy.sh` pushing `docs/.vuepress/dist` to `gh-pages`), `netlify.toml` publish dirs, and CI workflows must now publish Blume's `dist/`. Settings kept in a host's dashboard (Netlify's publish directory, build command, and `NODE_VERSION`) can't be committed: list them for the user, with a full (not shallow) clone if you set `lastModified: \"git\"`.\n- `.gitignore`: replace the docs' own entries (`.vuepress/dist`, the old `dest`) with `.blume` and `dist`. Keep entries other packages in the repo still use (`.temp`, `.cache`).\n- A `404.md` → delete it (as in VitePress).\n\n## Dropped — report these\n\nEverything vitepress.md's list covers that this site used, plus: v1 Markdown slots, `@flowstart` diagrams that weren't redrawn, the PWA and service worker, `sidebarDepth` and heading-only (`auto`) sidebars, `nextLinks` / `prevLinks` when only one was off, v1 `<code-block active>` defaults that couldn't move, Vue 2 components that weren't ported, `enhanceApp` / client-config code, `.vue` pages, `pageClass` and layout components, Stylus/Sass palette overrides, Universal Analytics ids, the DocSearch index, GitLab and Bitbucket edit links, old-site anchors that were already broken, hope-only components and frontmatter, and any `note` / `info` text that v1 showed literally and now renders as a callout.\n",".agents/skills/blume-update-docs/SKILL.md":"---\nname: blume-update-docs\ndescription: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.\n---\n\n# Update Blume Docs\n\nBlume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, `blume build` validates frontmatter and duplicate routes, and `blume validate` checks links and anchors.\n\nYour job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.\n\n## Ground rules\n\n- **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.\n- **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.\n- **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.\n- **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.\n- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.\n\n## Workflow\n\n1. **Establish context.**\n   - Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.\n   - Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.\n   - If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.\n\n2. **Reuse or create a maintenance branch.**\n   - If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.\n   - Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.\n\n3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:\n   - Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.\n   - Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.\n   - Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.\n   - Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.\n\n4. **Update the docs.**\n   - Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.\n   - Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.\n\n5. **Verify.**\n   - Run the docs build (`blume build` or the repo's documented docs QA) — it validates frontmatter and duplicate routes.\n   - Run `blume validate` to check internal links and anchors.\n   - Run lint/format/typecheck when the repo's conventions call for them on docs changes.\n   - Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.\n\n6. **Deliver.**\n   - **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.\n   - **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.\n\n## Resources\n\n- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.\n",".agents/skills/blume-update-docs/references/audit-checklist.md":"# Audit checklist\n\nUse this checklist to decide whether a docs-maintenance run should make changes.\n\n## Sources to check\n\n- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.\n- **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.\n- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.\n- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.\n- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.\n- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.\n- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.\n- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.\n\n## Change criteria\n\nMake a docs edit when at least one condition is true:\n\n- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.\n- A documented workflow no longer works or misses a required step.\n- A page promises a feature, provider, adapter, or integration the code no longer supports.\n- A new user-facing capability shipped but is absent from the appropriate docs page.\n- A link points at moved, removed, or outdated primary documentation.\n- A changelog or release page needs an entry for shipped user-facing behavior.\n\nSkip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.\n\n## Blume editing guidance\n\n- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.\n- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.\n- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.\n- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.\n- Keep internal links root-relative (`/guides/setup`), in the form nearby pages already use.\n- Do not edit generated `.blume/` or `dist/` output.\n\n## PR notes\n\nInclude these sections in the PR body or no-op summary:\n\n- Sources checked\n- Docs changed\n- Verification run (commands and results)\n- Skipped checks, with reasons\n- Remaining risk or follow-up\n",".agents/skills/blume/SKILL.md":"---\nname: blume\ndescription: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.\n---\n\n# Blume\n\nBlume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.\n\nThe core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.\n\n## What makes it different\n\n- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.\n- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional in-page **assistant** or an **MCP server** so coding agents read your docs directly.\n- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.\n- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.\n\n## Quickstart\n\nBlume needs **Node.js 22.19 or newer**. From an empty folder:\n\n```bash\nnpx blume init   # scaffold docs/index.mdx, blume.config.ts, and package.json scripts, then install\nnpm run dev      # dev server with hot reload\nnpm run build    # static HTML to dist/, with a local search index\n```\n\nIn a project that already has a `package.json`, `blume init` leaves it alone: add `\"dev\": \"blume dev\"` and `\"build\": \"blume build\"` to its scripts, or run `npx blume dev`. Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.\n\n### Writing a page\n\nEvery page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.\n\n```mdx\n---\ntitle: Introduction\ndescription: Welcome to my docs.\n---\n\nWelcome! Use **Markdown** and built-in components — no imports required:\n\n:::note\nBlume ships callouts, cards, tabs, steps, and more.\n:::\n```\n\nNavigation, search, and page metadata are inferred from your files as you add them.\n\n## Upgrading from Blume 1\n\nBlume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the assistant's model backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), Ask AI is renamed the assistant (`ai.ask` becomes `ai.assistant`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:\n\n```bash\nnpx blume@latest upgrade\n```\n\nIt bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--codex` or `--claude` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.\n\n## Migrating from another framework\n\nTo move a Mintlify, Docusaurus, GitBook, MkDocs, ReadMe, or other docs site to Blume, the user runs `npx blume migrate [source] --codex` (or `--claude`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.\n\nEvery build publishes a generated agent skill for the site at `/skill.md` (`agents.skillMd`). To write a richer one from the docs, the user runs `blume skill --codex` (or `--claude`), which opens an agent on the `blume-write-skill` skill. When you are that agent, or the user asks you to write the site's skill directly, follow `skills/blume-write-skill/SKILL.md` in the installed package.\n\n## What's included\n\n- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.\n- **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).\n- **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.\n- **Narration** — a \"Listen to this page\" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build; `openai({ model, voice })` uses OpenAI's speech models instead, and with a `baseUrl` a self-hosted OpenAI-compatible speech server, and needs `@ai-sdk/openai` installed).\n- **Navigation** — inferred from files, refined with `meta.ts` or config.\n- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.\n- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.\n- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.\n\n## How it works\n\nThe Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.\n\n## Full documentation\n\nThis is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the `docs/` directory bundled inside the installed `blume` package.\n\n**Locate the package first — it is not always at the repository root.** In a workspace monorepo (pnpm especially), `blume` is installed in the depending workspace's `node_modules` (e.g. `apps/docs/node_modules/blume/docs`), not the root. From the package that depends on `blume`, this prints the exact location:\n\n```bash\nnode -e \"console.log(require.resolve('blume/package.json'))\"\n```\n\nThe docs sit in `docs/` next to that `package.json`. Start with `docs/index.mdx` (Introduction) and `docs/01-quickstart.mdx`, then list the `docs/` directory: each section is a folder — configuration, content authoring, API references, discoverability (SEO and the agent-facing surface), the CLI, and advanced topics — so open the one that covers the task.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository\n\noRPC — typesafe API framework. pnpm monorepo (Node 22+, `pnpm` workspaces) with packages in `packages/*`, docs/website in `apps/content` (Blume), example apps in `playgrounds/*`, root-level e2e tests in `tests/`, and benchmarks in `benches/`.\n\n## Commands\n\n```bash\npnpm install                 # setup\npnpm test                    # all tests: per-package tests + root vitest\npnpm vitest run <path>       # single test file (from repo root), e.g. pnpm vitest run packages/server/src/builder.test.ts\npnpm type:check              # tsc across all packages — this is also what runs the *.test-d.ts type tests\npnpm lint                    # eslint (also the formatter — @antfu/eslint-config)\npnpm lint:fix\npnpm bench                   # vitest bench\npnpm --filter @orpc/server prepack # build one package (unbuild, runs automatically on pack/publish); builds are NOT needed for dev/tests\n```\n\n- Root vitest config (`vitest.config.ts`) runs all `*.test.ts` with `globals: true`, plus a jsdom project for `*.test.tsx` in `packages/next`, `packages/tanstack-query`, `packages/pinia-colada` and `packages/swr`.\n- Exceptions: `packages/bun` runs with `bun test`, `packages/cloudflare` with its own vitest (workerd) — both are excluded from the root vitest and root tsconfig. Run their tests via `pnpm --filter <pkg> test`. `packages/nest` is excluded only from the root tsconfig (type-check it with `pnpm --filter @orpc/nest type:check`); its tests run in the root vitest.\n- Docs site (Blume): `cd apps/content && pnpm dev` (`blume dev`); `pnpm build` runs `blume build`. Content lives in `.mdx` files; custom pages in `apps/content/pages/`. The dev server caches content-collection frontmatter for custom pages — restart it after editing blog-post frontmatter. `pnpm docs:validate` (root) runs the JSDoc backlink checker plus `blume validate --strict` (links, anchors, assets); CI runs it too.\n- PR titles follow Conventional Commits. Scope rules:\n  - Use the package name (e.g. `feat(server): ...`), or `rpc`/`openapi` for changes tied to a protocol rather than a single package, such as its serializers, handlers, links, or dedicated plugins.\n  - Use `content` only for the docs site itself (styles, Blume config, ...), and only with the `chore` type — never `feat`/`fix`/`perf`/`docs`, since these changes do not affect library users. Documentation about a package or protocol uses that package/protocol scope instead (e.g. `docs(openapi): ...`).\n  - Omit the scope when the change is too broad for a single one.\n","packages/ai-sdk/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ai-sdk\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fai-sdk?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/arktype/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/arktype\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Farktype?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/bun/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/bun\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fbun?logo=npm\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/client/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/client\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fclient?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/cloudflare/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/cloudflare\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcloudflare?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/contract/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/contract\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcontract?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/effect/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-effect\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-effect?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/evlog/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/evlog\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fevlog?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/hibernation/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/hibernation\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fhibernation?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/json-schema/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/json-schema\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fjson-schema?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/lock/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-lock\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-lock?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/mcp/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/mcp\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmcp?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/msw/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/msw\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmsw?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/nest/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/nest\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnest?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/next/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/next\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnext?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/node/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/node\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnode?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/openapi/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/openapi\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopenapi?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/opentelemetry/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/opentelemetry\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopentelemetry?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/pinia-colada/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pinia-colada\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpinia-colada?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/pino/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pino\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpino?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/publisher/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/publisher\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpublisher?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/ratelimit/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ratelimit\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fratelimit?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/server/README.md":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/server\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fserver?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","packages/shared/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fshared%2FREADME.md -->","packages/swr/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fswr%2FREADME.md -->","packages/tanstack-query/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftanstack-query%2FREADME.md -->","packages/trpc/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftrpc%2FREADME.md -->","packages/valibot/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fvalibot%2FREADME.md -->","packages/zod/README.md":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fzod%2FREADME.md -->","skills/orpc-contract/SKILL.md":"---\nname: orpc-contract\ndescription: \"Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill.\"\nlicense: MIT\n---\n\n# oRPC Contract-First\n\nContract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with `oc` from `@orpc/contract`, and an implementation built from it with `implement` from `@orpc/server`. Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the `os`-first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with `unlazyRouter` first).\n\nThis skill targets oRPC v2. The `orpc` skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see [Full documentation](#full-documentation)).\n\n## Define the contract with oc\n\nEvery chain is optional and each call returns a new instance, so share base contracts freely. A contract has no `.handler`. Zod, Valibot, ArkType, and any other Standard Schema library work.\n\n```ts\nimport { oc } from '@orpc/contract'\nimport * as z from 'zod'\n\nexport const contract = {\n  planet: {\n    list: oc\n      .output(z.array(z.object({ id: z.number(), name: z.string() }))),\n    find: oc\n      .errors({ NOT_FOUND: {} })\n      .input(z.object({ id: z.number() }))\n      .output(z.object({ id: z.number(), name: z.string() })),\n  },\n}\n```\n\n- Always define `.output`: without it clients infer the output as `unknown`.\n- A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n- Attach shared metadata to a whole subtree with `oc.meta(someMeta).router({...})`.\n- `.errors({ NOT_FOUND: {} })` declares typesafe errors; implementations throw them via `errors.NOT_FOUND()` and clients infer their shapes.\n- Repeated `.input`/`.output` calls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields.\n- Schema-library-free contracts use the `type` utility from `@orpc/contract`: `oc.input(type<{ value: number }>())`, optionally with a mapping function as `type<Input, Output>(fn)`.\n- REST routes attach via `.meta(openapi({ method: 'GET', path: '/planets/{id}' }))` from `@orpc/openapi`, exactly as on `os`; routing rules, `prefix`, and spec generation belong to the `orpc-openapi` skill.\n\nInfer types with `InferRouterContractInputs`, `InferRouterContractOutputs`, and `InferRouterContractErrors` from `@orpc/contract`.\n\n## Implement with implement\n\n`implement` turns the contract into an implementer that mirrors its shape and type-checks every handler; `.router` also enforces the contract at runtime.\n\n```ts\nimport { implement } from '@orpc/server'\n\nconst implementer = implement(contract).$context<{ db: DB }>()\n\nconst listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())\n\nconst findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {\n  const planet = await context.db.find(input.id)\n  if (!planet)\n    throw errors.NOT_FOUND()\n  return planet\n})\n\nexport const router = implementer.router({\n  planet: { list: listPlanets, find: findPlanet },\n})\n```\n\n- `.$context` declares the initial context the procedures require, as on `os`.\n- Apply middleware per procedure with `.use(mw)` before `.handler`. That runs after input validation (the contract already registered `.input`); to wrap validation, apply it at router level: `implementer.use(mw)` for every procedure, or `implementer.planet.use(mw).list` for a subtree. Router-level plus procedure-level `.use` can run the same middleware twice; use the dedupe pattern from the `orpc` skill.\n- `implementer.middleware(fn)` creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the `in` operator: `if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS()`. Any type-compatible middleware also works.\n- The result is a normal router: serve it with `RPCHandler` (`orpc` skill) or `OpenAPIHandler` (`orpc-openapi` skill), call it in-process with `call` or `createRouterClient` from `@orpc/server`.\n\n## Consume the contract from clients\n\n`RPCLink` needs only the contract type; `OpenAPILink` takes the contract as a runtime value to read each procedure's route. Get the client types exactly right:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\n// RPC protocol (server side is RPCHandler)\nconst rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })\nconst client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)\n\n// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)\nconst openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })\nconst apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)\n```\n\n- Router-first equivalents use `RouterClient<typeof router>` from `@orpc/server` in the same positions.\n- `JsonifiedClient` is required over `OpenAPILink` because OpenAPI serialization is one-way (a `Date` returns as a string); dropping it via Smart Coercion, plus `OpenAPILink` options and CORS caveats, are in the `orpc-openapi` skill.\n- Per-call client context is the second type parameter, `RouterContractClient<typeof contract, ClientContext>`, then `client.planet.find(input, { context: { token } })`; link options like `headers` accept functions of that context.\n- Export `RouterContractClient<typeof contract>` as a type from the server package so clients never import the contract module itself (still needed as a runtime value for `OpenAPILink`; ship the minified JSON below).\n- In very large codebases, skip the root client: pin each procedure with `.meta(meta.path([...]))` and build per-procedure clients with `createContractClientFactory` from `@orpc/contract` (`createContractJsonifiedClientFactory` from `@orpc/openapi` when the link needs `JsonifiedClient`); fetch `contract/client-factory` before adopting it.\n\n## Ship the contract\n\nWhen the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:\n\n```ts\nimport fs from 'node:fs'\nimport { minifyRouterContract } from '@orpc/contract'\nimport { unlazyRouter } from '@orpc/server'\n\nconst minified = minifyRouterContract(await unlazyRouter(router))\nfs.writeFileSync('./contract.json', JSON.stringify(minified))\n```\n\n`minifyRouterContract` keeps only client-needed metadata. On the client, import the JSON and cast, since schemas do not survive serialization: `new OpenAPILink(contract as typeof router, ...)`.\n\nTo publish a typed SDK to npm, export a factory that pairs the contract with a link:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nexport function createMyApi(apiKey: string): RouterContractClient<typeof contract> {\n  const link = new RPCLink({\n    origin: 'https://example.com',\n    url: '/rpc',\n    headers: { 'x-api-key': apiKey },\n  })\n  return createORPCClient(link)\n}\n```\n\nBundle with `tsdown --dts src/index.ts`, point `exports` at `dist` types plus import entries, list `@orpc/client` and `@orpc/contract` as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch `recipes/publish-client-to-npm` for the complete `package.json`.\n\n## Generate the contract from an existing OpenAPI spec\n\nUse Hey API's `orpc` plugin instead of hand-writing the contract. Install `@hey-api/openapi-ts@next` as a dev dependency (oRPC v2 output requires the `next` tag until the next stable Hey API release) and create `openapi-ts.config.ts`:\n\n```ts\nimport { defineConfig } from '@hey-api/openapi-ts'\n\nexport default defineConfig({\n  input: 'https://example.com/openapi.json', // local file or URL\n  output: 'src/contract',\n  plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],\n})\n```\n\nThen run `npx @hey-api/openapi-ts`. It writes `orpc.gen.ts` (one procedure contract per operation, routed via `.meta(openapi({...}))` with `inputStructure: 'detailed'`, plus a combined `contract` router) and `zod.gen.ts`. The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install those too. From there, implement the contract on your own server, or point `OpenAPILink` at the existing spec-compliant server.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/contract/procedure.md)\n\nPages to fetch when you need details beyond this skill:\n\n- Contract: `contract/procedure`, `contract/router`, `contract/implementation`, `contract/generate-from-openapi`\n- Clients: `client/client-side`, `client/server-side`, `client/error-handling`, `openapi/link`\n- Workflows: `recipes/publish-client-to-npm`, `contract/client-factory`, `recipes/monorepo-setup`\n","skills/orpc-migrate/SKILL.md":"---\nname: orpc-migrate\ndescription: \"Migrate existing codebases to current oRPC, covering tRPC to oRPC (incremental wrapping via the @orpc/trpc integration or a full rewrite with the concept mapping) and oRPC v1 to v2 (package renames, breaking changes, and a safe order of operations). Use when asked to migrate from tRPC to oRPC, convert or wrap a tRPC router, upgrade oRPC v1 to v2, fix oRPC v2 breaking changes, or swap `@trpc/*` packages for `@orpc/*` equivalents. Biases toward retrieval from the oRPC docs over pre-trained knowledge. Not for greenfield oRPC work or new features in an already-migrated codebase: use the orpc skill for those.\"\nlicense: MIT\n---\n\n# Migrating to oRPC\n\nPlaybook for two migrations: tRPC to oRPC, and oRPC v1 to v2. Work in small mechanical steps and run the project's typecheck and test suite after each one, so any failure points at the last step. Pretrained knowledge of oRPC describes v1 and is often wrong for v2: derive every import path, builder method, and option name from the docs pages listed at the end, never from memory. For core v2 concepts while rewriting (the `os` builder, routers, middleware, clients), load the `orpc` skill. v2 currently ships under the `beta` npm dist-tag and plain installs get v1; drop the `@beta` suffix once `npm view @orpc/server dist-tags` shows `latest` at 2.x.\n\n## tRPC to oRPC\n\nTwo paths. Pick incremental when the app must keep shipping or the tRPC router is large; pick the full rewrite when the router is small enough to convert in one pass.\n\n### Incremental: wrap the existing tRPC router\n\nInstall `@orpc/trpc@beta` and convert. The result is a regular oRPC router: expose it through an RPC or OpenAPI handler, or call it with a server-side client, while the tRPC code keeps working untouched.\n\n```ts\nimport { toORPCRouter } from '@orpc/trpc'\n\nconst orpcRouter = toORPCRouter(trpcRouter)\n```\n\n- tRPC error formatting is not supported: tRPC errors arrive wrapped in `ORPCError` with the `TRPCError` as `cause` (and a `ZodError` below that for validation failures). Reshape them in a handler interceptor if consumers need structured errors.\n- `toTRPCMeta` bridges oRPC `openapi()` metadata into tRPC `.meta()` so converted procedures get OpenAPI routing. Chained tRPC `.meta()` calls merge shallowly, so keep all oRPC metadata inside a single `toTRPCMeta` call.\n\nThen rewrite leaf routers to native oRPC one at a time, mounting each next to the converted router in one plain object.\n\n### Full rewrite: concept map\n\n| Concept           | tRPC                                           | oRPC                           |\n| ----------------- | ---------------------------------------------- | ------------------------------ |\n| Router            | `t.router({...})`                              | plain object                   |\n| Procedure builder | `t.procedure`                                  | `os`                           |\n| Context           | `initTRPC.context<T>()`                        | `os.$context<T>()`             |\n| Create middleware | `t.middleware(fn)`                             | `os.middleware(fn)`            |\n| Use middleware    | `.use(mw)`                                     | `.use(mw)`                     |\n| Validation        | `.input(schema)` / `.output(schema)`           | same names                     |\n| Implementation    | `.query()` / `.mutation()` / `.subscription()` | `.handler()` for all three     |\n| Errors            | `new TRPCError({ code, ... })`                 | `new ORPCError(code, { ... })` |\n| Serializer        | `superjson` transformer                        | built in, remove `superjson`   |\n\nSteps, in order, verifying after each:\n\n1. **Packages.** Remove `@trpc/server`, `@trpc/client`, `@trpc/tanstack-react-query`; install `@orpc/server@beta`, `@orpc/client@beta`, `@orpc/tanstack-query@beta`.\n2. **Base file.** Port the context factory unchanged, then rebuild the shared procedures. In handlers and middleware, `ctx` becomes `context`:\n\n   ```ts\n   import { ORPCError, os } from '@orpc/server'\n\n   const o = os.$context<Awaited<ReturnType<typeof createContext>>>()\n\n   export const publicProcedure = o.use(timingMiddleware)\n   export const protectedProcedure = publicProcedure.use(({ context, next }) => {\n     if (!context.session?.user)\n       throw new ORPCError('UNAUTHORIZED')\n     return next({ context: { session: context.session } })\n   })\n   ```\n\n3. **Procedures.** Replace `.query`/`.mutation`/`.subscription` with `.handler`; `.input` and `.output` carry over as is.\n4. **App router.** Delete every `createTRPCRouter()` wrapper; nested plain objects are the router.\n5. **Server.** Replace the tRPC adapter with an oRPC handler for the runtime (fetch shown; other adapters exist for Node, Fastify, AWS Lambda, WebSocket):\n\n   ```ts\n   import { RPCHandler } from '@orpc/server/fetch'\n\n   const handler = new RPCHandler(appRouter)\n\n   const { response } = await handler.handle(request, {\n     prefix: '/api/orpc',\n     context: await createContext({ headers: request.headers }),\n   })\n   ```\n\n6. **Client.** `RPCLink` plus `createORPCClient`, typed by `RouterClient`. Call sites drop the `.query()`/`.mutate()` suffixes:\n\n   ```ts\n   import type { RouterClient } from '@orpc/server'\n   import { createORPCClient } from '@orpc/client'\n   import { RPCLink } from '@orpc/client/fetch'\n\n   const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })\n   export const client: RouterClient<typeof appRouter> = createORPCClient(link)\n\n   const { planets } = await client.planet.list({ cursor: 0 })\n   ```\n\n7. **TanStack Query.** `createTanstackQueryUtils(client)` replaces the provider and `useTRPC` hook entirely; use the utils object directly. Input moves inside an `input` key: `orpc.planet.list.queryOptions({ input: { cursor: 0 } })`, `orpc.planet.create.mutationOptions()`. For infinite queries, `infiniteOptions` takes `input` as a function of the page param.\n\n## oRPC v1 to v2\n\nMost v1 names still compile through deprecated aliases (strike-through hints, not errors), so migrate in passes. Order of operations:\n\n1. **Update packages.** Install every `@orpc/*` package from the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`, and so on). Swap renamed ones first: `@orpc/react-query`/`@orpc/vue-query`/`@orpc/solid-query`/`@orpc/svelte-query` all became `@orpc/tanstack-query`; `@orpc/openapi-client` merged into `@orpc/openapi`; `@orpc/react` became `@orpc/next`; `@orpc/otel` became `@orpc/opentelemetry`; the `experimental-` packages were promoted (`@orpc/publisher`, `@orpc/ratelimit`, `@orpc/pino`, `@orpc/swr`); `@orpc/vue-colada` became `@orpc/pinia-colada`. Typecheck: the remaining errors are the hard breaks.\n2. **Fix the hard breaks** (no aliases):\n   - Routing: `.route`, `.prefix`, `.tag`, `.$route` are gone from the builder. Use `.meta(openapi({ method, path, prefix, tags }))` from `@orpc/openapi`, or restore `.route` with `import '@orpc/openapi/extensions/route'`.\n   - `.callable` and `.actionable`: use `call`/`createRouterClient` from `@orpc/server` and `createServerFunctionable` from `@orpc/next`, or the corresponding extension imports.\n   - `RPCLink`: the single `url` split into `origin` plus a path-only `url`.\n   - Errors: `status` was removed from `ORPCError` and `.errors` definitions; map codes to HTTP status with `errorStatusMap` on the handler.\n   - `safe()`: the third tuple element is now the typed error itself (or `null`) and a fourth `isSuccess` element was added.\n   - Option renames, scoped: handler `rootInterceptors` to `routingInterceptors` (handler `clientInterceptors` still exists, unchanged); link `clientInterceptors` to `transportInterceptors`. Flat `eventIterator*` options moved under the adapter's request/response mapping: `toFetchResponse.eventStream` on the fetch handler, `sendStandardResponse.eventStream` on Node, `toFetchRequest.eventStream` on the link.\n   - `adapterInterceptors` was removed from handlers and links, because regular interceptors can now customize body parsing behavior.\n3. **Audit silent behavior changes** (compile fine, behave differently):\n   - **Wire format changed:** a v1 link cannot talk to a v2 server, in either direction. Deploy the upgraded server and clients together.\n   - **Automatic middleware deduplication removed:** middleware applied at both router and procedure level now runs twice, with no warning. Guard shared middleware with the context-flag pattern from the dedupe-middleware recipe.\n   - **Batch Plugin `exclude` became `filter` with the opposite meaning.** Usually delete `exclude`; if skipping is still needed, negate the predicate.\n   - **`RPCHandler` rejects GET by default** (`allowMethods` defaults to POST/PUT/PATCH/DELETE). Simplest fix: stop sending GET from the link; only allow GET deliberately, with CSRF protection.\n   - **Handler `filter` takes positional arguments now;** the v1 destructured form still type-checks but reads wrong values.\n   - **`.input`/`.output` now stack:** a repeated call adds a schema instead of replacing the previous one.\n4. **Sweep deprecated aliases** last: `eventIterator` to `asyncIteratorObject`, handler plugins gained a `HandlerPlugin` suffix and link plugins a `LinkPlugin` suffix, `ContractRouter*` types became `RouterContract*`. The from-v1 guide ends with the full alias cheat sheet.\n\nVerification: typecheck and unit tests after steps 1, 2, and 4; step 3 needs integration or e2e tests, since those changes never surface at compile time. Before finishing, grep for old package names and remaining deprecation strike-throughs.\n\n## Docs retrieval\n\nFetch pages instead of recalling them, and if this skill and a fetched page disagree, trust the page. The v2 docs live at https://orpc.dev and the v1 docs at https://v1.orpc.dev; slugs look alike across both hosts, so check which host a page came from before copying anything from it. The index of every v2 docs page is at https://orpc.dev/llms.txt, https://orpc.dev/llms-full.txt bundles the entire docs in one large file, and appending `.md` to any page URL returns its exact source markdown.\n\nAuthoritative pages to consult during the migration (this skill deliberately omits their full mapping tables):\n\n- https://orpc.dev/docs/migrations/from-trpc : side-by-side tRPC/oRPC code for every step, including server setup per framework\n- https://orpc.dev/docs/migrations/from-v1 : every v2 breaking change with v1/v2 comparisons, package rename table, and the deprecated alias cheat sheet\n- https://orpc.dev/docs/integrations/trpc : `toORPCRouter` and `toTRPCMeta` reference for the incremental path\n","skills/orpc-openapi/SKILL.md":"---\nname: orpc-openapi\ndescription: \"Expose an oRPC router as a spec-compliant OpenAPI HTTP API. Use when a project depends on @orpc/openapi, or for defining REST-style routes on oRPC procedures (openapi() metadata or .route with method, path, successStatus), serving them with OpenAPIHandler alongside RPCHandler, coercing query and path strings with Smart Coercion, calling an OpenAPI-shaped API with OpenAPILink, generating an OpenAPI 3.2 (or 3.1, 3.0) document with OpenAPIGenerator, or serving Scalar or Swagger docs with the OpenAPI Reference plugin. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For contract-first work (defining contracts with oc, implementing them with implement, or generating a contract from an existing OpenAPI spec), use the orpc-contract skill; for plain RPC serving, core builder, middleware, or client work with no REST exposure, use the orpc skill instead.\"\nlicense: MIT\n---\n\n# oRPC over OpenAPI\n\noRPC procedures speak two protocols from one router: the RPC protocol (`RPCHandler`/`RPCLink`) and plain OpenAPI HTTP (`OpenAPIHandler`/`OpenAPILink`). This skill covers the OpenAPI side. For core builder, middleware, context, and client concepts, load the `orpc` skill (it also carries the v2 install and version check); for contract-first workflows with `@orpc/contract`, load the `orpc-contract` skill.\n\nThis skill targets oRPC v2. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its exact docs page first (see [Full documentation](#full-documentation)).\n\n## Routing\n\nA procedure defaults to `POST` with a path derived from the router structure (`planet.create` becomes `POST /planet/create`). Override with `openapi` metadata, on server (`os`) and contract (`oc`) builders alike:\n\n```ts\nimport { openapi } from '@orpc/openapi'\nimport { os } from '@orpc/server'\nimport { z } from 'zod'\n\nconst getPlanet = os\n  .meta(openapi({ method: 'GET', path: '/planets/{id}', successStatus: 200 }))\n  .input(z.object({ id: z.string() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n- Path params: put `{id}` in `path` and the same key as a required field in the input schema. Use `{+path}` for catch-all segments that may contain `/`.\n- `prefix` prepends a path to a procedure or a whole router: `os.meta(openapi({ prefix: '/api/v2' })).router({...})`. Always set a `prefix` on lazy routers so lazy loading only triggers for relevant requests.\n- Merging across repeated `.meta(openapi(...))` calls: `prefix` and `tags` concatenate, `method`/`path`/`successStatus` last-wins, and setting a field to `undefined` resets it to the default.\n- `successStatus` defaults to `200` and must be below 400.\n\nFor direct routing without `.meta(openapi(...))`, enable the `.route` extension with a side-effect import in a module that always runs at startup (base builder file or server entry):\n\n```ts\nimport '@orpc/openapi/extensions/route'\n\nconst ping = os.route({ method: 'GET', path: '/ping' }).handler(async () => 'pong')\n```\n\n## Input and output mapping\n\nCompact mode (default): path params merge with query params or the request body depending on the HTTP method, so `GET /planets/earth?q=life` yields input `{ id: 'earth', q: 'life' }`. The handler's return value becomes the response body with status `successStatus`.\n\nSet `inputStructure: 'detailed'` in the metadata to receive `{ params, query, headers, body }` instead (define only the fields you need in the input schema). Set `outputStructure: 'detailed'` to return `{ status?, headers?, body? }` and vary the status per response.\n\nQuery strings and form data are decoded with bracket notation (fetch `openapi/bracket-notation` before designing schemas for nested query input; its limits are not guessable):\n\n- Repeated keys become arrays: `?color=red&color=blue` gives `['red', 'blue']`; `color[]=red` pushes too.\n- `[number]` targets an array index; `[key]` targets an object property: `?filter[status]=active` gives `{ filter: { status: 'active' } }`.\n- It cannot represent empty objects or arrays, root-level arrays, or objects whose keys are all numbers, and query/form values always arrive as strings (or files, in form data).\n\nOverride decoding per parameter with `paramsStyles` (`'primitive'`, `'comma-delimited-array'`, `'comma-delimited-object'`) and `queryStyles` (those plus `'array'`, `'json'`, and space/pipe-delimited variants). Use `requestBodyHint`/`responseBodyHint` (`'json'`, `'form-data'`, `'event-stream'`, `'octet-stream'`, `'file'`, ...) when headers alone cannot tell the parser how to handle the body, for example a raw `ReadableStream` upload.\n\n## Serving: OpenAPIHandler\n\nImport from the adapter subpath (`@orpc/openapi/fetch`, `@orpc/openapi/node`, ...):\n\n```ts\nimport { SmartCoercionHandlerPlugin } from '@orpc/json-schema'\nimport { OpenAPIHandler } from '@orpc/openapi/fetch'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new SmartCoercionHandlerPlugin({ converters: [new ZodToJsonSchemaConverter()] }),\n  ],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} })\n  return matched ? response : new Response('Not Found', { status: 404 })\n}\n```\n\n`OpenAPIHandler` coexists with `RPCHandler`: both accept the same router, so mount them on different prefixes (for example `/api` and `/rpc`) and try each in turn, returning the first `matched` response.\n\nSmart Coercion: query, path, and form values arrive as strings, so add `SmartCoercionHandlerPlugin` whenever input schemas expect non-string types from those sources. It coerces schema-driven, lossless conversions only (`'123'` to `123`, `'true'`/`'on'` to `true`, ISO strings to `Date`, arrays to `Set`/`Map` via `x-native-type`) and leaves ambiguous values untouched. Skip it when you already coerce in the schema or performance is critical; it adds runtime overhead.\n\nOther handler options: `interceptors`/`routingInterceptors`/`clientInterceptors` for logging and error mapping, `filter` to exclude procedures from matching, and `errorStatusMap` plus `customErrorResponseBodyEncoder` to customize error responses (by default `ORPCError` codes map to statuses via `COMMON_ERROR_STATUS_MAP`, for example `NOT_FOUND` to 404).\n\n## Calling: OpenAPILink\n\n`OpenAPILink` calls an OpenAPI-shaped oRPC API (or any spec-compliant server) through a typesafe client. It needs the contract or router type to know each procedure's route:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\nconst link = new OpenAPILink(contract, {\n  origin: 'https://api.example.com',\n  url: '/api',\n  headers: ({ context }) => ({\n    authorization: context?.token ? `Bearer ${context.token}` : undefined,\n  }),\n})\n\nconst client: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)\n```\n\nWith a router instead of a contract, type the client as `JsonifiedClient<RouterClient<typeof router>>` (`RouterClient` from `@orpc/server`). `JsonifiedClient` exists because OpenAPI serialization is one-way: a `Date` returns as a string. Add `SmartCoercionLinkPlugin` from `@orpc/json-schema` (same converters, first argument is the contract) to restore native types on responses, then drop the `JsonifiedClient` wrapper from the client type.\n\nTo ship a contract to clients without bundling server code, minify it to JSON and import it with a cast; the flow is in the `orpc-contract` skill under \"Ship the contract\".\n\n## Spec document and interactive docs\n\n`OpenAPIGenerator` turns a router or contract into an OpenAPI 3.2 document (pass any `3.1.x` or `3.0.x` `version` for tools that stop at an older version; `QUERY` procedures need 3.2). `OpenAPIReferenceHandlerPlugin` serves the spec at `/spec.json` and a Scalar UI at `/` under the handler prefix (change with `specPath`/`docsPath`, or set `provider: 'swagger'` for Swagger UI):\n\n```ts\nimport { OpenAPIGenerator } from '@orpc/openapi'\nimport { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] })\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new OpenAPIReferenceHandlerPlugin({\n      spec: () => generator.generate(router, {\n        base: {\n          info: { title: 'Planet API', version: '1.0.0' },\n          servers: [{ url: '/api' }], // absolute URL in production\n        },\n      }),\n    }),\n  ],\n})\n```\n\nEnrich the document through `openapi` metadata: `operationId`, `summary`, `description`, `tags`, `successDescription`, and a `spec` callback that receives the generated operation object and returns an extended one (security requirements, extra responses). Write `spec` and `base` as OpenAPI 3.2 objects even when generating 3.1 or 3.0; the generator downgrades the whole document. Converters also exist for Valibot (`@orpc/valibot`) and ArkType (`@orpc/arktype`); schemas without a matching converter fall back to Standard JSON Schema conversion.\n\nVerify the wiring before declaring success: request `/spec.json` under the handler prefix and one routed endpoint, and confirm the method, path, and status you configured.\n\n## Contract-first\n\nContract-first workflows belong to the `orpc-contract` skill: defining the shape with `oc` from `@orpc/contract`, implementing it with `implement` from `@orpc/server`, consuming the contract from clients, shipping it as minified JSON, and generating it from an existing OpenAPI spec with Hey API. On the OpenAPI side a contract behaves exactly like a router: attach routes with `.meta(openapi({...}))` on `oc` as shown in [Routing](#routing), serve the implemented router with `OpenAPIHandler` as usual, generate the spec document from the contract directly, and give `OpenAPILink` the contract as its runtime value.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/openapi/routing.md)\n\nPages to fetch when you need details beyond this skill:\n\n- OpenAPI: `openapi/routing`, `openapi/input-and-output-mapping`, `openapi/bracket-notation`, `openapi/serializer`, `openapi/handler`, `openapi/link`, `openapi/specification`, `openapi/scalar`\n- Plugins: `plugins/smart-coercion`, `plugins/openapi-reference`\n","skills/orpc/SKILL.md":"---\nname: orpc\ndescription: \"Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.\"\nlicense: MIT\n---\n\n# oRPC\n\noRPC is a typesafe API framework: write plain TypeScript functions on the server, call them from clients like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step. The same router can also be served as a REST API with an OpenAPI spec.\n\nThis skill targets oRPC v2. Check what is installed before writing code: `npm ls @orpc/server` (or any `@orpc/*` package). A 1.x version means v1, where this skill's guidance does not apply; use the `orpc-migrate` skill to upgrade. v2 currently ships under the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`; a plain install silently gets v1). If `npm view @orpc/server dist-tags` shows `latest` at 2.x, the beta has ended: install normally.\n\nPretrained oRPC knowledge describes v1 and is often wrong for v2 (routing moved to `.meta(openapi(...))`, `RPCLink` split `url` into `origin` plus a path, automatic middleware dedupe was removed). Prefer retrieval: the index of every docs page is at https://orpc.dev/llms.txt; see [Full documentation](#full-documentation) for the mechanics.\n\nPackage map:\n\n- `@orpc/server`: the `os` builder, routers, middleware, `RPCHandler`, server-side clients (`call`, `createRouterClient`), `implement` for mocks\n- `@orpc/client`: `createORPCClient`, `RPCLink`, `safe`, `createSafeClient`, `isDefinedError`\n- `@orpc/contract`: contract-first API definitions implemented separately from their logic (see the `orpc-contract` skill)\n- `@orpc/openapi`: `OpenAPIHandler`, `OpenAPILink`, OpenAPI 3.2 spec generation\n- Integration packages such as `@orpc/tanstack-query` and `@orpc/nest`\n\n## Define procedures\n\nBuild a procedure with the `os` builder: describe input with a schema, implement with `.handler`. Zod, Valibot, ArkType, and any other [Standard Schema](https://standardschema.dev/) library work for `.input`, `.output`, and error `data`.\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const listPlanets = os\n  .handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments\n\nexport const findPlanet = os\n  .input(z.object({ id: z.number() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n`.handler` is the only required step. The full chain, every other step optional:\n\n```ts\nconst example = os\n  .$context<{ headers: Headers }>() // initial context this procedure requires\n  .errors({ NOT_FOUND: {} }) // typed errors\n  .use(requireAuth) // middleware\n  .input(z.object({ id: z.number() }))\n  .output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking\n  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))\n```\n\nEvery builder step returns a new instance, so share base builders freely: `const authed = os.use(requireAuth)` then build many procedures from `authed`.\n\n## Assemble a router\n\nA router is a plain object mapping keys to procedures (or nested routers). Do not use the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n\n```ts\nexport const router = {\n  planet: { list: listPlanets, find: findPlanet },\n  admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree\n  planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router\n}\n```\n\nInfer types with `InferRouterInputs` / `InferRouterOutputs` from `@orpc/server`. Applying `.use` at both router and procedure level can run the same middleware twice; see the dedupe pattern below.\n\n## Middleware and context\n\nContext comes from two places. Initial context is declared with `.$context` and passed explicitly when serving or calling (environment values: headers, env, db). Injected context is added at runtime by middleware via `next({ context })` (runtime values: the authenticated user).\n\n```ts\nimport { ORPCError, os } from '@orpc/server'\n\nconst base = os.$context<{ headers: Headers }>()\n\nconst requireAuth = base.middleware(async ({ context, next }) => {\n  const user = await parseUser(context.headers)\n  if (!user) {\n    throw new ORPCError('UNAUTHORIZED')\n  }\n  return next({ context: { user } }) // handler now sees context.user, typed non-null\n})\n```\n\n`.use` accepts named middleware or inline functions. Middleware registered before `.input` runs before validation, the rest after. Middleware can also declare typed input (`os.middleware(async ({ next }, id: number) => ...)`); adapt mismatched shapes with `.use(mw.adaptInput(input => input.id))`.\n\nBest practice, dedupe expensive middleware: the same middleware can run twice in one call (router-level plus procedure-level `.use`, or a procedure `call`ing another). Cache the result in context:\n\n```ts\nconst authProvider = os\n  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()\n  .middleware(async ({ context, next }) => {\n    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)\n    return next({ context: { auth, authLoaded: true } })\n  })\n```\n\n## Typesafe errors\n\nThrow `ORPCError` (a `code` plus optional `message` and `data`). Both `message` and `data` are sent to the client, so never put secrets in them. Throw only `Error` instances, never literals.\n\nDefine errors with `.errors` so clients can infer each error's shape:\n\n```ts\nconst find = os\n  .errors({\n    NOT_FOUND: { message: 'Planet not found' }, // default message\n    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },\n  })\n  .handler(async ({ input, errors }) => {\n    throw errors.NOT_FOUND()\n  })\n```\n\n`throw new ORPCError('NOT_FOUND')` inside that handler is converted to the matching typed error when code and data match. Convert custom error classes to `ORPCError` in a middleware `try/catch`.\n\n## Serve with RPCHandler\n\n`RPCHandler` matches requests to procedures, validates input, runs handlers, and encodes results. Pick the adapter for your runtime. Fetch API (Bun, Deno, Cloudflare Workers):\n\n```ts\nimport { onError } from '@orpc/server'\nimport { RPCHandler } from '@orpc/server/fetch'\nimport { CORSHandlerPlugin } from '@orpc/server/plugins'\n\nconst handler = new RPCHandler(router, {\n  plugins: [new CORSHandlerPlugin()],\n  interceptors: [onError(error => console.error(error))],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, {\n    prefix: '/rpc',\n    context: { headers: request.headers }, // provide the router's initial context here\n  })\n  return matched ? response : new Response('Not found', { status: 404 })\n}\n// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workers\n```\n\nNode HTTP:\n\n```ts\nimport { createServer } from 'node:http'\nimport { RPCHandler } from '@orpc/server/node'\n\nconst handler = new RPCHandler(router)\n\nconst server = createServer(async (req, res) => {\n  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })\n  if (matched)\n    return\n  res.statusCode = 404\n  res.end('Not found')\n})\n\nserver.listen(3000)\n```\n\nUnmatched requests fall through to your own handling. By default `RPCHandler` accepts only `POST`, `PUT`, `PATCH`, and `DELETE`; enabling `GET` via `allowMethods` is a CSRF risk with cookie auth, see [RPC Handler](https://orpc.dev/docs/rpc/handler). Handler options also include `interceptors`, `routingInterceptors`, `clientInterceptors`, `plugins`, `filter`, and `errorStatusMap`. Adapters also exist for [AWS Lambda](https://orpc.dev/docs/adapters/aws-lambda), [Fastify](https://orpc.dev/docs/adapters/fastify), [WebSocket](https://orpc.dev/docs/adapters/websocket), [Message Port](https://orpc.dev/docs/adapters/message-port), and [Expo](https://orpc.dev/docs/adapters/expo).\n\n## Call procedures\n\nServer side (same process, no HTTP; also the fastest way to test procedures):\n\n```ts\nimport { call, createRouterClient } from '@orpc/server'\n\nconst planet = await call(findPlanet, { id: 1 }, { context: { headers } })\n\nconst client = createRouterClient(router, { context: { headers } }) // context can be a function\nconst planets = await client.planet.list()\n```\n\nClient side, `RPCLink` turns calls into HTTP requests. Import the router as a type only so no server code reaches the client bundle:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from '../server/router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({\n  origin: 'http://127.0.0.1:3000',\n  url: '/rpc', // must match the server's prefix\n  headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions\n})\n\nexport const orpc: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await orpc.planet.find({ id: 1 })\n```\n\nClient error handling: plain `try/catch` works, but `safe` preserves typed error inference:\n\n```ts\nimport { createSafeClient, isDefinedError, safe } from '@orpc/client'\n\nconst [error, data] = await safe(orpc.planet.find({ id: 1 }))\nif (isDefinedError(error)) {\n  console.log(error.code, error.data) // typed from the procedure's .errors\n}\nelse if (error) {\n  // unknown error\n}\n\nconst safeClient = createSafeClient(orpc) // every call returns [error, data]\n```\n\n## Beyond the basics\n\n- Streaming / SSE: return an async generator from `.handler`, validate events with `asyncIteratorObject`, resume via `lastEventId`: [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object)\n- OpenAPI: serve the same router as REST with routing metadata plus `OpenAPIHandler`, and generate a spec (covered in depth by the `orpc-openapi` skill): [OpenAPI Handler](https://orpc.dev/docs/openapi/handler)\n- Contract-first: define contracts with `@orpc/contract`, implement with `implement` (covered in depth by the `orpc-contract` skill): [Contracts](https://orpc.dev/docs/contract/procedure)\n- Plugins for handler and link: batch, CORS, dedupe, retry, compression, request limits, smart coercion, static files, timeout, tmp file upload, and more. Fetch a plugin's docs page before configuring it; option names are not guessable: [Plugins](https://orpc.dev/docs/plugins/batch)\n- Integrations: [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) (`createTanstackQueryUtils`), [SWR](https://orpc.dev/docs/integrations/swr), [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada), [Next.js](https://orpc.dev/docs/integrations/next), [NestJS](https://orpc.dev/docs/integrations/nest), [AI SDK](https://orpc.dev/docs/integrations/ai-sdk), [MCP](https://orpc.dev/docs/integrations/mcp) (`McpRegistrar` exposes procedures as MCP tools), [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry)\n- Testing: `call` procedures directly; mock with `implement(router.planet.list).handler(() => [])`, and run the project's typecheck before declaring success, since end-to-end types are oRPC's first correctness signal: [Testing and Mocking](https://orpc.dev/docs/recipes/testing-and-mocking)\n- Monorepos: TypeScript project references keep client types resolvable: [Monorepo Setup](https://orpc.dev/docs/recipes/monorepo-setup)\n- Migrating from tRPC or oRPC v1: use the `orpc-migrate` skill\n\n## Full documentation\n\nThis skill is an overview; fetch exact docs instead of guessing APIs, and if this skill and a fetched page disagree, trust the page. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/procedure.md)\n\nDoc map, all under `https://orpc.dev/docs/`:\n\n- Top level: `procedure`, `router`, `middleware`, `context`, `error-handling`, `metadata`, plus `binary-data` (file uploads) and `async-iterator-object` (streaming/SSE)\n- `rpc/*`, `openapi/*`: protocol details, handlers, links; `contract/*`: contract-first (the `orpc-contract` skill)\n- `client/*`: server- and client-side clients, error handling, `DynamicLink`\n- `adapters/*`: per-runtime serving quirks (fetch-api, node-http, aws-lambda, fastify, websocket, message-port, expo)\n- `plugins/*`: twenty handler/link plugins; `helpers/*`: cookie, encryption, form-data, lock, publisher, ratelimit, signing, base64url\n- `integrations/*`: framework glue; `recipes/*`: guidance (testing, SSR, monorepos, validation)\n- `migrations/from-v1`, `migrations/from-trpc`: upgrades (use the `orpc-migrate` skill)\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository\n\noRPC — typesafe API framework. pnpm monorepo (Node 22+, `pnpm` workspaces) with packages in `packages/*`, docs/website in `apps/content` (Blume), example apps in `playgrounds/*`, root-level e2e tests in `tests/`, and benchmarks in `benches/`.\n\n## Commands\n\n```bash\npnpm install                 # setup\npnpm test                    # all tests: per-package tests + root vitest\npnpm vitest run <path>       # single test file (from repo root), e.g. pnpm vitest run packages/server/src/builder.test.ts\npnpm type:check              # tsc across all packages — this is also what runs the *.test-d.ts type tests\npnpm lint                    # eslint (also the formatter — @antfu/eslint-config)\npnpm lint:fix\npnpm bench                   # vitest bench\npnpm --filter @orpc/server prepack # build one package (unbuild, runs automatically on pack/publish); builds are NOT needed for dev/tests\n```\n\n- Root vitest config (`vitest.config.ts`) runs all `*.test.ts` with `globals: true`, plus a jsdom project for `*.test.tsx` in `packages/next`, `packages/tanstack-query`, `packages/pinia-colada` and `packages/swr`.\n- Exceptions: `packages/bun` runs with `bun test`, `packages/cloudflare` with its own vitest (workerd) — both are excluded from the root vitest and root tsconfig. Run their tests via `pnpm --filter <pkg> test`. `packages/nest` is excluded only from the root tsconfig (type-check it with `pnpm --filter @orpc/nest type:check`); its tests run in the root vitest.\n- Docs site (Blume): `cd apps/content && pnpm dev` (`blume dev`); `pnpm build` runs `blume build`. Content lives in `.mdx` files; custom pages in `apps/content/pages/`. The dev server caches content-collection frontmatter for custom pages — restart it after editing blog-post frontmatter. `pnpm docs:validate` (root) runs the JSDoc backlink checker plus `blume validate --strict` (links, anchors, assets); CI runs it too.\n- PR titles follow Conventional Commits. Scope rules:\n  - Use the package name (e.g. `feat(server): ...`), or `rpc`/`openapi` for changes tied to a protocol rather than a single package, such as its serializers, handlers, links, or dedicated plugins.\n  - Use `content` only for the docs site itself (styles, Blume config, ...), and only with the `chore` type — never `feat`/`fix`/`perf`/`docs`, since these changes do not affect library users. Documentation about a package or protocol uses that package/protocol scope instead (e.g. `docs(openapi): ...`).\n  - Omit the scope when the change is too broad for a single one.\n","isInternal":false,"tokens":690,"sizeBytes":2652},{"name":"docsify.md","path":".agents/skills/blume-migrate/references/docsify.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/docsify.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Docsify → Blume\n\nDocsify has no build step. One `index.html` sets `window.$docsify = {…}`, loads Docsify and its plugins from a CDN, and renders each page's Markdown in the browser. Watch for five traps:\n\n- **URLs live after a `#`** (`/#/guide/setup?id=install`). A server never sees that part, so `redirects` can't match an old URL. The site needs a client-side redirect script (URLs below).\n- **Links resolve from the docs root**, not the page's folder, unless `relativePath: true`. Images and includes resolve from the page's folder either way, even with a leading `/`. Raw HTML `href`/`src` resolve from the docs root, because the document URL is always the folder holding `index.html`.\n- **Pages have no frontmatter.** The title is usually the first `# H1`, but not always.\n- **Navigation is a Markdown list** (`_sidebar.md`), and its groups usually sit flat in one folder.\n- **Heading ids differ from Blume's**, and old deep links carry them as `?id=`.\n\n**Fingerprint the version.** It changes callouts and heading ids. `docsify@4`, or any script URL with `/lib/` (even an unversioned `…/npm/docsify/lib/docsify.min.js`), runs v4 code: the v5 package still ships the v4 build under `lib/`. `docsify@5/dist/…` or `new Docsify({…})` is v5.\n\n## Detect\n\n- An **`index.html`** whose inline script sets **`window.$docsify`** (v5 can also use `new Docsify({…})`), usually in `docs/`. The folder holding it is the content root.\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `.nojekyll` beside it, with `README.md` as the home page.\n- `docsify-cli` or `docsify` in `package.json`. Without either, the source has to be named: `npx blume migrate docsify`.\n- A GitHub wiki has `_Sidebar.md` and `Home.md` but no `index.html`. It isn't Docsify.\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that runs `docsify serve <dir>` (usually the repo root), so the default `content.root: \"docs\"` matches. With no manifest, scaffold one there.\n\nRead the whole `index.html`: the `$docsify` object (it can be a function), every `<script src>` (each is a plugin), the `<head>`, and the `<body>`. Open a few routes on the live site too. **A page whose include target is missing rendered blank in Docsify 4**, so some \"pages\" never worked.\n\n## Run the codemod first\n\nOn the Docsify folder, from the directory that will hold `blume.config.ts`, dry-run the bundled codemod, then apply it once, before any hand edits:\n\n```bash\ncurl -s https://api.github.com/emojis > emojis.json   # only if pages use :shortcodes:\nnode <skill>/scripts/docsify-codemod.mjs docs\nnode <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old \\\n  --group-folders --emoji emojis.json --site https://docs.example.com docs\n```\n\nIt's zero-dependency and idempotent, and reads `version`, `relativePath`, `routerMode`, `noEmoji`, `homepage`, `loadSidebar`, `loadNavbar`, and `coverpage` from `index.html` (override with `--version` and `--relative-path`). It converts:\n\n- `!>` → `:::warning`, `?>` → `:::tip`, and `> [!TYPE]` alerts → directives, leaving fenced and indented code alone;\n- docsify-tabs → `<Tabs>`;\n- `':include'` lines → `<include>`, resolved from the page's folder as Docsify did, even with a leading `/`. A `:fragment=` is copied to its own file where Docsify cut one: code includes, plus Markdown ones in v5. Docsify 4 showed a Markdown include whole, so it stays whole;\n- `:id=` → `[#id]`, `{docsify-ignore}` → `[!toc]`, and setext headings → ATX;\n- links resolved the way Docsify resolved them (hash links, `?id=`, and `--site` absolute links included) → relative file links, without an anchor that named the target's title H1 (that heading becomes `title`);\n- images → rebased for moved pages. Docsify read a leading-slash image from the page's folder too, where Blume would read it from `public/`;\n- link and image attribute strings → dropped;\n- relative raw HTML URLs and `':ignore'` links → root paths. An `':ignore'` link to a page's own file opened the raw Markdown, so it becomes a raw `<a href>` to the page's Markdown copy (`/<new route>.md`): a Markdown link naming a page's file, even root-relative, lands on the page itself;\n- emoji shortcodes → Unicode;\n- the H1 or sidebar label → `title`, plus `sidebar.label`, `seo.title`, and `hidden: true`, with later H1s demoted;\n- `README.md` → `index.md`, and pages that need MDX → `.mdx` with MDX-safe comments, `<br>`, void tags, and autolinks.\n\n`--group-folders` moves each page into a folder per sidebar group, writes each folder's `meta.ts` in sidebar order plus a root `meta.ts` that keeps the groups in sidebar order, and writes the old → new route table to `docsify-routes.json`. Leave the flag off to keep the files where they are. With `homepage: \"intro.md\"`, the table also maps `/intro` → `/`, since Docsify served that page at both.\n\n`--snapshot` records every page's headings with the ids Docsify gave them, plus `old-routes.txt`. The pin step (URLs below) needs it, and it only works on the first run, while the pages are untouched. On Zenroom it matched Docsify 4.13.1's rendered ids on every page that rendered.\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each. The codemod never invents a fix: a missing include stays as written, and a file that must move to `public/` is only listed.\n\n## Config: `window.$docsify` → `blume.config.ts`\n\n| Docsify | Blume |\n| --- | --- |\n| `name` (may hold HTML) | `title`, as plain text. The `<title>` and `<meta name=\"description\">` in `index.html` give `title` (when `name` is unset) and `description` |\n| `logo` | `logo`, with the file moved to `public/`. Docsify shows the logo instead of the name, so a wordmark needs `logo: { image, text: \"\" }`. Check it in dark mode |\n| `nameLink` | `logo.href` |\n| `repo` | `github: { owner, repo }` (with `branch` from an edit-on-github URL). A non-GitHub repo goes in `navigation.actions` |\n| `themeColor` (v4), `--theme-color` in a stylesheet (v5) | `theme.accent` |\n| A dark-only theme (`dark.css`, a themeable `*-dark.css`) | `theme.mode: \"dark\"`. Other themes map to nothing |\n| Fonts in the stylesheet or `<link>`s | `theme.fonts` |\n| `<link rel=\"icon\">` | Copy the file to `public/favicon.<ext>` |\n| `<meta>` verification tags | `seo.metatags`. `twitter:site` goes to `seo.x.handle`. Drop `og:*` and `twitter:` card tags, since Blume writes them |\n| Custom CSS | A root `theme.css` with only the rules whose markup still exists. Docsify selectors (`.markdown-section`, `.sidebar`, `.cover`) match nothing |\n| Site URL (`CNAME`, a sitemap) | `deployment.site`. GitHub Pages isn't auto-detected |\n| `subMaxLevel: N` | `toc: { maxHeadingLevel: N }` when N ≥ 2 and isn't 3. Drop `maxLevel` |\n| `homepage: \"x.md\"` | Handled by the codemod. A remote URL needs vendoring |\n| `alias` | Local-to-local entries go in the route table. Remote targets: see Includes and aliases. Drop sidebar plumbing (`'/.*/_sidebar.md'`) and any alias whose target doesn't exist |\n| `externalLinkTarget: '_blank'` (explicit) | `markdown: { externalLinks: true }`. Docsify opens external links in a new tab even when this is unset, so report the change |\n| `notFoundPage` / `_404.md` | Blume's 404 page, or a custom `pages/404.astro` |\n| `formatUpdated`, `{docsify-updated}`, a last-modified plugin | `lastModified: \"git\"` (and `fetch-depth: 0` in CI) |\n| `collapsibleSidebarGroups` (v5) | `navigation.sidebar.display: \"group\"`, plus `collapsed: false` in each `meta.ts` that should start open |\n| `vueComponents`, `vueMounts`, `vueGlobalOptions`, `executeScript` | An island per widget (`islands/Name.vue`, `docs/content/islands.mdx`). Report |\n| `markdown` (marked options or a `renderer`) | Drop, and report what the renderer did. Common ones are mermaid (now native) and a `json` pretty-printer: minified JSON includes now render as one line, so fix that in whatever generates them. `breaks: true` has no equivalent |\n| `routes` (virtual routes) | Static ones become pages. Report dynamic ones |\n| `auto2top`, `topMargin`, `maxLevel`, `cornerExternalLinkTarget`, `requestHeaders`, `noCompileLinks`, `sidebarPosition`, `keyBindings`, `skipLink`, the search and pagination options, … | Drop |\n\n**Plugins** are `<script>` tags:\n\n- **Built in:** search, `docsify-copy-code`, `docsify-pagination`, and `zoom-image`. Delete them.\n- **Converted by the codemod:** emoji, `docsify-tabs`, and flexible-alerts.\n- **Prism language packs:** delete them. Shiki highlights the common languages in any case (` ```JSON ` works), and a fence in a language it doesn't know, or a misspelled one, renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: fix the name, or write `text`.\n- **Mermaid** plugins or renderers → native ` ```mermaid ` in `.mdx`.\n- **KaTeX** → `$$…$$` in `.mdx`, with inline `$…$` becoming `$$…$$` inside its sentence.\n- **`ga` / `gtag`** → `googleAnalytics({ id })` for a `G-` id. A `UA-` id no longer collects data, so report it. Other analytics → `script()`.\n- **Comments** (Disqus, Gitalk, giscus) → nothing built in. Use a `PageFooter` slot, or report them.\n- **Anything else** (bibtex, remote-markdown, demo boxes, custom inline plugins) → report.\n\n## Navigation: `_sidebar.md` → folders and `meta.ts`\n\n- **Groups over pages in one flat folder** (everything in `docs/` or `docs/pages/`) flatten silently. Use `--group-folders`: it moves each group's pages into a folder and writes `meta.ts` and the route table, and the hash script maps every old route. This departs from SKILL.md's `(group)/`-first rule: every Docsify URL changes form anyway (`/#/x` → `/x`), so the script runs for each old link either way, and the codemod rebases the moved pages' links and images. To keep URLs instead, move pages at the content root into `(group)/` folders, which add no URL segment; under a subfolder like `pages/`, they keep `/pages/x` but nest every group under a \"Pages\" group. Or keep the files and write an explicit `navigation.sidebar`.\n- **Groups whose pages already sit in a folder named for the group** (`Guide` → `guide/`) stay put, and `--group-folders` writes that folder's `meta.ts`. It moves the pages of a group whose folder has another name into one named for the group, so on a site already laid out in folders, leave the flag off and write each `meta.ts` by hand, with the group label and the sidebar's order.\n- **The home page** stays the root `index`. The generated sidebar lists it first, never inside a group.\n- **Loose pages listed after groups** can't keep that order, because Blume lists loose pages first. Move them into a folder, or report the change.\n- **External links, and links to files** like a PDF or HTML in `public/`, become `navigation.featured`. A file link there warns at build (\"no page matches it\"); the warning is harmless.\n- **A `_sidebar.md` inside a folder** replaced the sidebar under that folder. Map the folder to a `navigation.tabs` entry.\n- **Pages the sidebar doesn't list** get `hidden: true` from the codemod. They stay reachable, as before.\n- **Sidebar link titles** (`[x](x.md \"Tab title\")`) become `seo.title`. Report any that look copy-pasted from another page: Docsify showed them as the browser tab title.\n\n## Navbar and cover page\n\n- **`_navbar.md`:** doc-section links become `navigation.tabs` (a nested list becomes a tab with `items`), and other links become `navigation.actions`. With `mergeNavbar: true`, put the other links in `navigation.featured` instead, not in both: featured shows on every screen size, while actions hide on phones. Language links (`/zh-cn/`) become `i18n`.\n- **`_coverpage.md`** (shown above `README.md` on `/`; the whole home page with `onlyCover: true`): rebuild it as the top of `index.md`. The `# name <small>1.2</small>` heading is already the title. The tagline is prose or `description`, the logo is a Markdown image, and the buttons become links or `<Card>`s. Drop the background image or `![color](#…)`, and report it. Use `mode: center` when the cover was the whole page.\n\n## Content: what the codemod leaves to you\n\n**Titles.** Docsify drops only the first H1 from its sidebar headings, so pages often have several. The codemod takes a leading H1 as the title and demotes every later heading from the second H1 on. When content comes before the first H1, it takes the sidebar label as the title and demotes everything. Check every title it flags: about one page in five on Zenroom started with a section (\"Key generation\", \"Intro\"). For those, use the sidebar label as the title, put the H1 back as an `##` section, and demote every heading.\n\n**MDX hazards** in a renamed page. The codemod lists each one, and all but the first fail the build:\n\n- An indented code block renders as a plain paragraph: fence it.\n- An HTML table whose cell tags share a line with text: give each `<tr>`/`<td>` its own line.\n- A block element inside `<p>`, typical of README banners: make the `<p>` a `<div>`.\n- A repeated attribute: keep one.\n- A brace in prose: escape it as `\\{`.\n\n**Includes and aliases.**\n\n- **A missing include target:** the page was blank on the old site. Repoint the include to the file's new name (a folder was usually renamed), or drop it, and report it.\n- **A fragment copied from a generated file** goes stale. Have the generator write one file per fragment.\n- **An included file's own H1** becomes a second `<h1>`: remove it.\n- **An alias to a raw URL of a file in the same repo** (`'/CHANGELOG': 'https://raw.githubusercontent.com/<owner>/<repo>/main/CHANGELOG.md'`) becomes a page that `<include>`s the file through a symlink in `_snippets/`. Removing that file's H1 changes a file people read on GitHub, so say so.\n- **Other remote aliases and includes:** vendor a copy (with a source comment), or use `mdxRemote()`.\n- **iframe, video, and audio embeds** become raw HTML, with the file in `public/`.\n\n**Files the pages link** (listed under \"must be served from public/\"): Docsify served every file in its folder at its path. Blume publishes a file a page links relatively with that page, but at a new URL, and the codemod writes Docsify's root-resolved links as root paths, which keep the old URLs and are served from `public/`.\n\n- Copy each file into `public/` at the same path.\n- If a generator writes the files (test fixtures, examples), symlink the folder instead: `public/_media/examples → ../docs/examples`. Astro copies the target, and `validate` checks the links.\n- An HTML document saved as `.md` (generated API docs) goes to `public/` as HTML.\n- `blume validate --strict` reports a missing one as `BLUME_BROKEN_ASSET`.\n\n**Everything else:**\n\n- **Link attributes** the codemod dropped (`:target`, `:size`, `:no-zoom`, `:class`): restore any that matter as raw HTML (`<img src=\"/x.png\" width=\"200\" data-no-zoom />`, with the file in `public/`).\n- **Emoji shortcodes left over:** paste the emoji.\n- **Page `<script>`s:** Docsify 4 ran them only with `executeScript` or Vue, so they often never ran. Move one that did into an island.\n- **Vendored READMEs** keep Blume's ids (they had GitHub-style ids already), and they aren't in the snapshot.\n\n## URLs and redirects\n\n**Hash mode (the default).** Every page lived at `/#/<route>`, also reachable as `/#/<route>.md` and `/index.html#/<route>`, with folder indexes at `#/<folder>/` and headings at `?id=<id>`. The server only saw `/`.\n\n**A. Blume takes over the old address** (the usual case). `docsify-routes.json` holds the page moves the codemod made. Add an entry for each alias you turned into a page. Routes that now land on a file in `public/` go in a separate `files` map that only the script reads: a redirect page would sit where the file is. Name a `public/` folder with its slash (`/api/`) or name the file itself (`/api/index.html`): static hosts, `blume dev`, and `blume preview` serve its `index.html` at `/api/` (and redirect `/api` there), where its relative links resolve.\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport moved from \"./docsify-routes.json\" with { type: \"json\" };\n\n// Old routes that now land on a file in public/: the script sends readers\n// there, but a redirect can't, so they stay out of `redirects`.\nconst files = {}; // e.g. { \"/onepage\": \"/onepage.md\" }\n\n// Docsify served every page at /#/<route>; a server never sees the hash.\n// Swap an old hash URL for its new route, and ?id=<heading> for #<heading>.\nconst base = \"\"; // the site's deployment.base plus basePath, if it has either\nconst docsifyRedirects = `(() => {\n  const base = ${JSON.stringify(base)};\n  const routes = ${JSON.stringify({ ...moved, ...files })};\n  const go = () => {\n    const hash = location.hash;\n    if (!hash.startsWith(\"#/\")) return;\n    const q = hash.indexOf(\"?\");\n    let path = q === -1 ? hash.slice(1) : hash.slice(1, q);\n    const id = q === -1 ? null : new URLSearchParams(hash.slice(q + 1)).get(\"id\");\n    try { path = decodeURI(new URL(path, \"http://x\").pathname); } catch {}\n    path = path.replace(/\\\\.md$/i, \"\").replace(/(^|\\\\/)README$/i, \"$1\");\n    if (path.length > 1) path = path.replace(/\\\\/+$/, \"\");\n    // location.origin keeps a crafted #//other.host/ link on this site.\n    location.replace(location.origin + base + (routes[path] ?? path) + (id ? \"#\" + id : \"\"));\n  };\n  go();\n  addEventListener(\"hashchange\", go);\n})();`;\n\nexport default defineConfig({\n  redirects: Object.entries(moved).map(([from, to]) => ({ from, to })),\n  integrations: [\n    {\n      name: \"docsify-hash-redirects\",\n      hooks: {\n        \"astro:config:setup\": ({ injectScript }) => {\n          injectScript(\"head-inline\", docsifyRedirects);\n        },\n      },\n    },\n  ],\n});\n```\n\nThe script runs from every page's `<head>`, the 404 page included, before the body loads, in `blume dev` and in every build. Keep its two safeguards: `new URL(…).pathname` resolves the `./` and `../` segments Docsify left in links (`#/./guide`), and `location.origin` stops a crafted `#//other.host/` from sending readers off the site. Unmoved routes need no entry. The `redirects` also cover direct visits to a moved path and its `/old.md` Markdown twin. Don't use the `script()` analytics adapter for this: it's emitted in production builds only, and held until consent when `consent` is set.\n\n**B. The old site keeps its own address** (an old GitHub Pages URL, while the docs move to a new domain). Replace the old `index.html` with a page running the same parsing, with the new origin in place of `location.origin`, and a `<noscript><meta http-equiv=\"refresh\" content=\"0; url=https://new.example/\"></noscript>` fallback.\n\n**History mode** (`routerMode: 'history'`). URLs were real paths, so the route table and `redirects` carry moved pages. Anchors were still `?id=`, so add this after `go();`:\n\n```js\nif (!location.hash) {\n  const id = new URLSearchParams(location.search).get(\"id\");\n  if (id) location.replace(location.pathname + \"#\" + id);\n}\n```\n\n**Keep old heading anchors.** Docsify 4 leaks entity names into ids (`Foo & Bar` → `foo-amp-bar`, `Don't` → `don39t`), collapses dash runs, keeps emoji and shortcode names, and prefixes a leading digit with `_`. Docsify 5 is closer to Blume, but still differs in that prefix, in non-ASCII capitals, and where a heading has emphasis (`_When_` stays in the id) or a shortcode. After the first build, pin each old id where it differs, and rebuild until it reports 0:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json --write\n```\n\nOn Zenroom, one pass kept every old id except those of title H1s (58 of 968), and a link to one of those lands at the top of the page anyway. A page that never rendered on the old site (a missing include) has nothing to pin.\n\n**Check every old route** against `blume preview`. In `.docsify-old/old-routes.txt`, a folder route is listed without its slash, but Docsify served it only with one. Both forms reach the script.\n\n```bash\nnode -e '\nconst routes = { ...require(\"./docsify-routes.json\") };   // add the files map too\nconst old = require(\"fs\").readFileSync(\".docsify-old/old-routes.txt\", \"utf8\").split(\"\\n\").filter(Boolean);\n(async () => { for (const r of old) {\n  const res = await fetch(\"http://localhost:4321\" + (routes[r] ?? r));\n  if (res.status !== 200) console.log(res.status, r);\n} })();'\n```\n\nThen open a few old hash URLs, with and without `?id=`, in a browser on the deployed site. A plain `curl` can't test the hash script.\n\n**GitHub Pages**, where most Docsify sites live:\n\n- Switch the Pages source from the branch folder to GitHub Actions, using the workflow in https://useblume.dev/guides/markdown-docs-github-pages (with `fetch-depth: 0` for `lastModified: \"git\"`).\n- Pages has no redirect rules of its own. A static build writes each exact `redirects` entry as a page that forwards to the new URL, which is all the table holds; a pattern redirect wouldn't work there.\n- A project URL (`user.github.io/repo/`) becomes `deployment: { site: \"https://user.github.io\", base: \"/repo\" }`, and that base goes in the script too. Blume adds the base to root paths in Markdown and raw HTML alike, so the codemod's root paths need nothing more.\n- The custom domain stays in the Pages settings. Neither `CNAME` nor `.nojekyll` is needed.\n- The workflow's `npm ci` needs a lockfile, so commit one.\n\n## Teardown\n\nAfter harvesting what they declare, delete:\n\n- `index.html`;\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `_404.md` (Blume ignores `_` files meanwhile), or the files `loadSidebar`, `loadNavbar`, and `coverpage` name instead. Without the `_`, Blume publishes them as pages until they're gone;\n- `.nojekyll`, the theme CSS, and generators that read the sidebar or Docsify syntax (sitemaps, one-page builds).\n\nSwap `docsify-cli`/`docsify` for `blume` and `docsify serve` for `blume dev`/`blume build`/`blume preview`. Keep `docsify-routes.json` and `.docsify-old/` until the pins are done and old links stop mattering.\n\n## Dropped — report these\n\n- The cover's background and the navbar's language links (now `i18n`).\n- The home page's place in a sidebar group, and loose pages after groups.\n- Link and image attributes you didn't restore.\n- Fragments now frozen as copies.\n- Vendored or `mdxRemote()` aliases.\n- Pages that were blank on the old site (missing includes) and what you did with each.\n- Title H1 anchors (they land at the page top).\n- Comment plugins, a `UA-` analytics id, Vue widgets, and page scripts.\n- Custom `markdown` renderers (JSON pretty-printing included) and `breaks: true`.\n- Dynamic virtual `routes`.\n- Theme CSS with no Blume target, and copy-pasted sidebar link titles now in `seo.title`.\n- New-tab external links, unless you set `markdown.externalLinks`.\n","isInternal":false,"tokens":5997,"sizeBytes":22703},{"name":"docus.md","path":".agents/skills/blume-migrate/references/docus.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/docus.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Docus → Blume\n\nDocus is a Nuxt layer for docs sites. It's built on Nuxt Content (pages are Markdown files in `content/`, routed by their path) and Nuxt UI (the theme, and the prose components pages use). Navigation is the file tree plus per-folder `.navigation.yml` files, and settings live in `app.config.ts` and `nuxt.config.ts`. Pages are written in **MDC**, Nuxt Content's Markdown-with-components syntax. The bundled codemod converts most of it; these traps are why it exists:\n\n- **MDC's `::note` … `::` looks like a Blume directive and isn't one.** Blume prints `::note` and its `::` closer as text (in `.md`, every MDC construct), and `blume check` and `build` warn `BLUME_MDC_SYNTAX` at each block opener and inline component (`:badge[New]{color=\"primary\"}`) left in either format: convert them as below.\n- **MDX is stricter than MDC.** `[text]{.class}` spans fail the build, and so do `{…}`, `<Name>`, and `<50` in prose. Prompt bodies are full of these.\n- **Icons are Iconify names** (`i-lucide-zap`, `i-simple-icons-github`). Blume renders nothing for them, and warns only for sidebar and tab icons.\n- **Docus sorts siblings by file path as text,** so `10.faq.md` lists between `1.` and `2.`. Blume sorts the prefixes as numbers.\n\nScope:\n\n- **Docus 3.x–5.x** is covered: the `docus` package on Nuxt Content 3 and Nuxt UI, whose content syntax is the same across the three. 5.x is current and is by far the most installed, and it's the one this reference was run on.\n- **Docus 1.x** (`@nuxt-themes/docus`, on Nuxt Content 2 with Nuxt Elements components) is flagged. Its section near the end was written from the package source, not tested on a site, and the codemod doesn't convert its components.\n- **The Nuxt UI docs template** (`nuxt-ui-templates/docs`, no `docus` dependency) writes the same content: the codemod and this reference apply, and its `app.config.ts` is read by hand.\n\n## Detect\n\n- **A `docus` dependency** means 3.x–5.x, and **`@nuxt-themes/docus`** means 1.x. It's usually in the docs package's own `package.json` (`docs/`, `apps/docs/`) in a module's monorepo.\n- Usually a `nuxt.config.ts` with `extends: ['docus']`. The 4.x starters have no config and run `nuxt dev --extends docus`, and 3.x ran `docus dev`.\n- A `content/` folder beside it with `.navigation.yml` files (3.x+) or `_dir.yml` files (1.x).\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that depends on `docus`, and set `content.root: \"content\"`. `public/` stays where it is: both tools serve it at the site root. In a workspace, read `references/monorepo.md` too.\n\n- **No `content/docs/` folder:** every file under `content/` is a page, and `content/index.md` is the landing page at `/`.\n- **A `content/docs/` folder:** Docus publishes only `content/docs/**`, under `/docs/…`, plus `content/index.md`. Exclude anything else under `content/` and report it. Docus hides the `docs` level in its sidebar. In Blume, add a tab `{ label: \"Docs\", path: \"/docs\" }`, or the section tabs below, or the sidebar wraps everything in a \"Docs\" group.\n- **`content/<locale>/` folders:** see i18n.\n\n## Record the old site first\n\nBefore editing anything, save the URL list, the old heading ids, and the record of what rendered:\n\n- **URLs:** the live `/sitemap.xml` (on an i18n site, an index of one sitemap per locale), plus every `routeRules` redirect source in `nuxt.config.ts`.\n- **HTML:** `nuxt generate` writes `.output/public`, one `<route>.html` per page plus a meta-refresh stub per redirect. Copy it outside the repo; it's `<old build>` below. Or point the anchor script at the live origin.\n- **Check components against the live site.** A local build can miss a component a Nuxt module registers in its own build: it shows as an empty tag (`<InstallButton …></InstallButton>`) while production renders it. Request live pages with a browser user agent (`curl -A 'Mozilla/5.0'`): some Docus deployments answer `curl` with Markdown.\n\n## Run the codemod first\n\nOn the `content/` directory, dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/docus-codemod.mjs content          # report only\nnode <skill>/scripts/docus-codemod.mjs --write content  # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent. Icon names are checked against Blume's Lucide set once `blume` is installed (or with `--lucide <icons.json>`).\n\n**What it does:**\n\n- **Pairs MDC blocks** as remark-mdc does (see MDC syntax).\n- **Converts the Nuxt UI prose components:**\n  - callouts → `:::type` directives, or `<Callout type icon>` when a callout's icon isn't its type's own\n  - `card-group`/`card`, `field-group`/`field` → `<ResponseField>`, `steps`, `tabs`, `accordion`, `collapsible`\n  - `code-group` → `<CodeGroup>`, or one `package-install` fence when the group only repeats an install command\n  - `code-collapse`, `code-tree`, `code-preview`, `prompt`, `badge`\n  - the inline `:icon`, `:kbd`, `:badge`, `:video`, and `:u-color-mode-image`\n- **Strips Markdown attributes:** `[text]{.class}` spans and `{…}` attributes go.\n- **Rewrites fence labels and frontmatter:** fence `[label]`s become titles, `navigation` → `sidebar`, `seo.ogImage` → `seo.image`, `links` → `related`, a body H1 → `title`, and `.navigation.yml` → `meta.ts`.\n- **Renames to `.mdx`** each page that now needs MDX, and makes its prose MDX-safe (see MDC syntax).\n\nEvery `REVIEW` line is your to-do list, including:\n\n- custom and landing-page components, left as written\n- blocks with a slot the conversion doesn't read (a card's `#footer`), left as written\n- card descriptions, which Nuxt UI's prose card never rendered\n- callouts that linked somewhere\n- icons outside Lucide\n- unclosed blocks and stray closers\n- `links` lists longer than `related` takes, and frontmatter keys Blume rejects\n- MDC blocks inside blockquotes\n- file names whose URL changes\n\n## Config: `app.config.ts` → `blume.config.ts`\n\nRead `app/app.config.ts` (`app.config.ts` at the project root on Nuxt 3), `nuxt.config.ts`, the CSS that `css` lists, and `.env.example`.\n\n| `app.config.ts` | Blume |\n| --- | --- |\n| `seo.title` | `title`. Docus falls back to `site.name` in `nuxt.config.ts`, then the `package.json` `name`, then the git repository's name, so carry the one the site showed |\n| `seo.description` | `description` (Docus falls back to the `package.json` `description`) |\n| `seo.titleTemplate` | drop. The default `%s - <site>` is Blume's; report a custom one |\n| `seo.schema` | drop (report). Blume writes its own JSON-LD |\n| `header.title` | `title`, when there's no logo |\n| `header.logo.{light,dark,alt}` | `logo: { image: { light, dark, alt }, text: \"\" }`. Docus shows the logo without the title. A monochrome SVG → `currentColor` and the string form (SKILL.md) |\n| `header.logo.display: 'wordmark'` | the `wordmark` files as the logo |\n| `socials` (Simple Icons slug → URL) | `footer.socials` for the platforms Blume lists (`twitter` → `x`). Others (`nuxt`, `npm`) → `footer.links`, or drop (report) |\n| `github` (`url`, `branch`, `rootDir`), or nothing | `github: { owner, repo, branch, dir }`. **Docus reads the git remote when `github` isn't set,** so edit links showed anyway: take `owner`/`repo` from the remote. `dir` = `rootDir`; `branch` only when it isn't `main`. `github: false` → leave it out |\n| `toc.bottom.links` | `navigation.featured` (`to` → `href`). Report the move |\n| `navigation.sub` | `navigation.tabs` (see Navigation). `'aside'` anchors have no Blume form; report |\n| `docus.locale` (not `en`) | a one-locale `i18n` block |\n| `docus.colorMode` | `theme.mode`. Blume always shows the toggle; report |\n| `assistant.faqQuestions` | `ai.assistant.suggestions` (`[{ label }]`; flatten categories) |\n| `ui.colors.primary` | `theme.accent`. Nuxt UI paints shade 500 in light mode and 400 in dark: a Blume preset of the same name, else `{ light, dark }` with those Tailwind values. **Not declared means Docus's emerald:** the site turns Blume's default color unless you offer `theme.accent: \"green\"`; report it either way |\n| `--ui-radius: 0` in CSS | `theme.radius: \"none\"` |\n| `toc.title`, `docus.shortcuts`, `search.fts`, other `assistant.*` and `ui.*` keys | drop (report the `ui` overrides) |\n\n| `nuxt.config.ts` | Blume |\n| --- | --- |\n| `site.url` / `NUXT_SITE_URL` | `deployment.site`, unless the host is Vercel or Netlify (SKILL.md) |\n| `routeRules` with `redirect` | `redirects` (see URLs and redirects) |\n| `@nuxtjs/i18n` and `i18n` | see i18n |\n| `llms.notes`, `llms.sections` | `agents.llmsTxt: { details }` as Markdown, if worth keeping; `llms.title`/`description` drop |\n| `mcp` | Docus serves an MCP server at `/mcp` (or `mcp.route`) unless `mcp: false` or `mcp.enabled: false`. To keep one: `agents.mcp: { enabled: true, name }` (and `route` when it isn't `/mcp`) plus a host adapter (server output). Blume's tools are fixed, so report the site's own tools and prompts in `server/mcp/` |\n| `docus.assistant` | Docus turns the assistant on when the build has `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`: check the live site for it. Keep it as `ai: { assistant: { enabled: true, provider: gateway({ model }) } }` (`gateway` from `blume/ai`; Docus's default model is `google/gemini-3-flash`) plus a host adapter, or drop it (report) |\n| `app.head`, `@vercel/analytics`, `@nuxtjs/plausible`, `@nuxt/scripts` analytics | the provider's factory from `blume/analytics` (`vercel()`, `plausible({ domain })`, `cloudflare({ token })`); other scripts → `script()`; `meta` → `seo.metatags`. `@vercel/speed-insights` has no adapter (report) |\n| `content.build.markdown` (`toc`, `highlight.theme`, plugins), `mdc.highlight.theme` | `toc` and `markdown.code.theme` only when they differ from the defaults; each remark/rehype plugin's syntax converted by hand (report) |\n| `css` | what still applies → a root `theme.css` against Blume's tokens |\n| `studio`, `agentDiscovery`, `ogImage`, `sitemap`, `robots`, `schemaOrg`, `image`, `icon`, `nitro`, `vite` | drop (report). Blume writes its own sitemap, robots, OG images, and JSON-LD |\n\n**A project `content.config.ts`** adds collections such as a blog or changelog. Those aren't docs pages: map a blog to `type: blog` and a changelog to `type: changelog` (or `githubReleases()`, SKILL.md Changelogs), and report anything else.\n\n**Theme overrides** (`AppHeader*.vue`, `AppFooter*.vue`, `Docs*.vue`) aren't content. Map what they add to config (`navigation.cta`, `navigation.actions`, `footer.links`, `banner`), but read their `v-if`s first: a button shown only on the landing page (`route.path === '/'`) belongs on the landing page, since Blume's `cta` shows site-wide.\n\n## Navigation\n\nThe codemod writes each `.navigation.yml` as a `meta.ts` (`title`, `icon`) and each page's `navigation` as `sidebar` (`title` → `label`, `icon`, `false` → `hidden`). What's left:\n\n- **A folder's `navigation: false`** hides it: set `sidebar.hidden: true` on its pages (the codemod reports it).\n- **A folder with an `index.md` but no `.navigation.yml`:** Docus labels it with the index page's title. Blume uses the humanized folder name, so set `meta.ts` `title` when they differ.\n- **Order:**\n  - Docus compares paths as text: `1.intro`, `10.faq`, `11.glossary`, `2.setup`. Blume sorts the numbers. In a folder with ten or more unpadded siblings, keep Blume's order (almost always what the author meant) and report the change.\n  - Docus also interleaves pages and subfolders. Blume lists a level's pages above its subfolders, so report a page that sorted after a folder.\n- **Display:** Docus draws every folder as an always-open group, which is Blume's default `flat` display.\n- **`navigation.sub`** (a header tab bar, or anchors in the sidebar) makes each top-level folder a section that scopes the sidebar. That's `navigation.tabs`: one `{ label, path, icon? }` per top-level folder with pages, with `label` and `icon` from its `.navigation.yml`. A section with no index page needs no `href`, since a Blume tab falls back to the section's first page, as Docus did.\n\n## Pages: frontmatter and titles\n\nThe codemod maps `navigation`, `links` (header buttons → `related` cards at the foot of the page), and `seo.ogImage`. It removes `layout: docs` and `sitemap`. What's left:\n\n- **No `description`:** when a page opens with a paragraph (after an optional `# H1`), Docus took it as the description, so it showed twice. Blume derives none. To keep the meta description without the repeat, set `seo.description` to that paragraph (optional; say which you did).\n- **`sitemap: false`** has no sitemap-only switch: add `seo: { noindex: true }` only when the page should also leave search engines.\n- **`toc: false`** → `mode: wide` (report), or drop. Another `layout` → a Blume `mode`.\n- **Other keys** Nuxt Content kept and Blume rejects: drop and report each one, or declare it with `frontmatter.extend` if a component you wrote reads it.\n- **Titles:** Docus renders frontmatter `title` in the page header, falling back to the first `# H1`, so a body H1 showed twice. The codemod moves a lone H1 into `title` and reports the rest.\n\n## MDC syntax\n\nThe codemod applies these rules; you need them for what it leaves. They're remark-mdc's behavior (checked against `remark-mdc@3.11.1`):\n\n- **Block component:** an opening line `::name`, optionally followed by `{attributes}` and nothing else, then Markdown, then a closing line of colons only.\n  - Props are inline (`{key=\"value\" flag .class #id}`) or a YAML block, from `---` right after the opener to the next `---`. A `:key='[…]'` holds JSON.\n  - Slots are `#name` lines inside the block.\n  - An inline component is `:name[text]{attrs}`; alone on a line, it renders as a block.\n- **Pairing:**\n  - Every opener nests, even at the same colon count.\n  - A closer closes the innermost open block **whose opener has exactly as many colons**, together with anything still open inside it. It must not be indented deeper than that opener. Any other colon line, or a `::` mid-line, is text.\n  - An unclosed block runs to the end of its parent, and a closer inside a code fence is code.\n  - Check the old HTML for a literal `::` to see what the live site already showed broken.\n- **MDX safety:** in a `.mdx` page, prose must escape `{` and `}` as `\\{` and `\\}`, and turn **any `<` followed by a non-space character** into `&lt;` (or `\\<`) unless it opens a real tag: `useFetch<User>()`, `<script setup>` written as text, `<50 ms`, `<= 5`. Case matters as in JSX: `<input>` is HTML, but `<Input>` is a component, so a generic like `useTool<Input>()` is text. HTML comments become `{/* … */}`, `<https://…>` autolinks become links, and `<br>` becomes `<br />`. Code spans and `$$` math stay as written.\n- **Code fences:** ` ```ts [file.ts]{2} ` → ` ```ts file.ts {2} ` (`title=\"Two words\"` for a label with spaces). Blume reads ` ```ts [file.ts]{2} ` and a multi-word title as written too, the range highlighting its lines, so this only tidies. Leave MDC samples inside fences alone: they're documentation.\n\n## What the codemod leaves\n\n- **Custom components:** `app/components/content/*.vue`, or a Nuxt module's. Read the SFC to see what it renders and which props and slots it reads, then convert it:\n  - to Markdown\n  - to a Blume component\n  - to a static `.astro` component registered in `components.ts`\n  - to the `.vue` file as an island in `islands/`, with `@astrojs/vue` and `vue` installed. `:items='[\"a\"]'` becomes `items={[\"a\"]}`, and named slots become props or children.\n\n  Report each one. A component that rendered nothing on the live site (an unknown tag) has nothing to convert.\n\n- **Callouts with `to`:** the whole callout was a link. Add the link to its text when it isn't there already.\n- **Card descriptions:** Nuxt UI's prose card renders neither a `description` prop nor a `#description` slot, so that text never showed. The codemod keeps it as the card's body and flags it: keep it, or delete it to match the old page.\n- **Blocks with a slot the conversion doesn't read** (a card's `#footer`, a tab's extra slot) stay as written: convert them by hand.\n- **`code-tree`** becomes a `<CodeGroup>` titled by file path. The tree view is gone; a `<FileTree>` before it can show the layout.\n- **`prompt`:** Blume has no `windsurf` or `claude` action, and no prompt icon (the codemod reports both).\n- **Prose about features that didn't carry over:** grep the content for the dropped MCP tool and prompt names, `docus`, `/.well-known/skills`, and `Studio`, and list every page that now says something false.\n\n## Landing page\n\n`content/index.md` renders full-width, with no sidebar, title, or table of contents. Its `title` and `description` feed only the meta tags. The codemod leaves its `u-page-*` and custom components as written.\n\n- **Markdown landing** → `index.mdx` with `mode: custom` (no title or sidebar rendered, so write the heading) and `sidebar.hidden: true` (Docus never listed it).\n- **A designed landing**, or `app/pages/index.vue` → `pages/index.astro` on `PageLayout`.\n\n| Nuxt UI | Blume, in the `mode: custom` page |\n| --- | --- |\n| `u-page-hero` (`title`, `description`, `links`, or slots) | `# Title`, a paragraph, and a line of Markdown links; the primary one can also be `navigation.cta` |\n| `u-page-section` | `## Title`, a paragraph, and its features as cards |\n| `u-page-feature`, `u-page-card` (`title`, `description`, `icon`, `to`) | `<Card title icon href>description</Card>` in `<CardGroup cols={3}>` |\n| `u-page-grid`, `u-page-columns` | `<CardGroup cols={3}>` |\n| `u-button`, `u-page-cta` | Markdown links; a heading and a paragraph |\n| `u-page-logos`, decorative `div`s (glows, gradients) | a row of images, or drop (report) |\n\n## Icons\n\nBlume resolves Lucide names only (`zap`, or `lucide:zap`); **anything else renders nothing.** The codemod converts `i-lucide-*` (aliases of renamed icons included) and reports the rest:\n\n- **Other sets** (`i-heroicons-*`, `i-ph-*`): the closest Lucide name, checked at [lucide.dev/icons](https://lucide.dev/icons).\n- **Brands** (`i-simple-icons-*`): Lucide has a few (`github`, `gitlab`, `slack`, `linkedin`, `youtube`), but **`i-simple-icons-x` is not Lucide's `x`, a close cross.** Otherwise, save the SVG under `public/` and pass its path (`icon=\"/icons/nuxt.svg\"`), or drop it (report).\n- **`i-custom-*`** (Docus's `app/assets/icons/*.svg`): copy the SVG to `public/icons/` and pass the path.\n\n## i18n\n\nDocus i18n is `@nuxtjs/i18n` (`defaultLocale`, `locales: [{ code, name }]`) plus one `content/<code>/` folder per locale, and it forces the `prefix` strategy: every locale is under its code, the default included, and `/` redirects to the default.\n\n- Move `content/<default>/` up to `content/` (a default-locale folder warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`). Keep the others as `content/<code>/`, and run the codemod on `content/`.\n- Write `i18n: { defaultLocale, locales: [{ code, label }], hideDefaultLocalePrefix: false }`, `label` = `name`. That keeps every `/en/…` URL. Nothing serves `/` then, so add `{ from: \"/\", to: \"/<default>\", status: 302 }` as Docus did.\n- **Regional codes are lowercase in Docus URLs** (`zh-TW` → `/zh-tw/…`): use the lowercase code as Blume's `code` and folder.\n- Leave out any configured locale with no `content/<code>/` folder: Docus skipped it.\n\n## URLs and redirects\n\nDocus routes a page by its path under `content/`. It strips an `N.` prefix from each segment, drops `index`, and slugifies (lowercase, spaces → `-`). Blume strips `N.`, `N-`, and `N_` prefixes and `index` the same way, so **most URLs carry over unchanged**, with no trailing slash or `.html` to redirect. The codemod reports the exceptions:\n\n- **A dash prefix** (`01-intro.md`) stays in the Docus URL and Blume strips it: pin `slug` or redirect.\n- **Uppercase letters or spaces** were lowercased by Docus: rename the file.\n- **A version name** (`1.x`, `2.0`) stays whole in Docus: pin `slug`.\n- **`_` files** are pages in Docus and excluded by Blume: rename them.\n\n**Redirects:**\n\n- **`routeRules` → `redirects`.** A string `redirect` is a 307 in Nitro (`status: 307`); `{ to, statusCode }` → `{ to, status }`. A `/**` key strips its base: `'/old/**': { redirect: '/new/**' }` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`.\n- **`/raw/<route>.md`** is the Markdown copy of each page, which Docus links from `llms.txt`. Blume serves it at `/<route>.md`, so one rule covers it: `{ from: \"/raw/:path*\", to: \"/:path*\" }`. A `routeRules` redirect between two `/raw/…md` paths should target Blume's copy directly (`/raw/old.md` → `/new.md`), or it chains through that rule.\n- **Agent skills:** a `skills/` folder Docus published under `/.well-known/skills/` → `agents.skills: \"./skills\"`. Blume publishes them under `/.well-known/agent-skills/`, a skill with references as one `.tar.gz`. Redirect the old index to the new one, and report the change. Links can name the new index (`/.well-known/agent-skills/index.json`): `blume validate` accepts the files Blume generates.\n- **Anchors:** MDC slugs headings like Blume (`github-slugger`), then collapses repeated dashes, trims edge dashes, and prefixes a leading digit with `_`. `## Name & Title` is `#name-title` in Docus and `#name--title` in Blume; `## 1. Install` is `#_1-install`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old <old build>`, `--write`, and rebuild until it reports 0. On a server build (`vercel()`, `node()`) it reads the pages from `dist/client` itself.\n- **Verify** every old URL: a built page, or a redirect `from` whose target is built (SKILL.md Verification).\n\n## Deploy and teardown\n\n- **Keeping the assistant or the MCP server** needs server output: `deployment: vercel()` (or `node()`, `netlify()`, `cloudflare()`). `blume preview` can't serve a `vercel()` build, so check it with `blume dev`, then a preview deployment.\n- **Vercel with `vercel()`:**\n  - The build writes the Build Output API folder `.vercel/output` in the Blume project, not `dist/`.\n  - In the project's `vercel.json`, set `\"framework\": null` and the install and build commands (`references/monorepo.md` §4 for a workspace), and **no `outputDirectory`**. `framework: null` overrides a project still set to the Nuxt preset.\n  - In the dashboard, set the Root Directory to the docs package and Node to 22.19 or later. Set `AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token.\n  - With Turborepo, add `.vercel/output/**` to the docs' build outputs.\n  - List the dashboard settings as steps for the user.\n- **Dependencies:** remove `docus`, `nuxt`, the `@nuxt/*`/`@nuxtjs/*` modules, `nuxt-studio`, `better-sqlite3`, `@iconify-json/*`, `tailwindcss` (if only the theme used it), mapped analytics packages, `@nuxt/eslint-config`, and `vue` (keep it with `@astrojs/vue` for islands). Keep what the site's tests or scripts still import.\n- **Scripts:** `dev`/`build`/`generate`/`preview` → `blume dev`/`build`/`build`/`preview`. **Delete `postinstall: nuxt prepare`**, which fails once Nuxt is gone.\n- **Files:** after harvesting, delete `nuxt.config.ts`, `app/`, `content.config.ts`, `server/` (report its routes), and a Nuxt `eslint.config.mjs`. The codemod leaves a `.navigation.yml` whose keys `meta.ts` can't hold.\n- **`tsconfig.json`:** a Nuxt one points into `.nuxt/`, and `blume build` then fails with `BLUME_TSCONFIG_EXTENDS`. Delete or replace it.\n- **`.gitignore`:** in the docs package's own file, `.nuxt`, `.output`, `.data` → `.blume`, `dist`.\n- **CI:** grep for `nuxt generate`, `nuxi`, `.output/public`, and `--extends docus`.\n\n## Docus 1.x (`@nuxt-themes/docus`)\n\nWritten from `@nuxt-themes/docus@1.15.1` and `@nuxt-themes/elements@0.9.5`. Everything above applies (run the codemod for fences, frontmatter, and the H1), with these differences:\n\n- **Config** is `app.config.ts` → `docus: { … }`:\n  - `title`, `description` → same keys\n  - `url` → `deployment.site`\n  - `socials` → `footer.socials`. Values are handles (`twitter: '@nuxt_js'`, `github: 'owner/repo'`), so build each URL.\n  - `github: { owner, repo, branch, dir }` → `github`\n  - `header.logo`/`title` → `logo`/`title`\n  - `aside.collapsed` → `navigation.sidebar.display: \"group\"`\n  - `footer.textLinks`/`iconLinks` → `footer.links`\n  - everything else → drop (report)\n- **Folders** use `_dir.yml` (`title`, `icon`, `navigation: false`, `navigation.redirect` → a redirect from the folder's route).\n- **Titles** are the body `# H1` (the codemod moves it).\n- **Frontmatter:**\n  - `layout: page` → `mode: custom` or `mode: center`\n  - `aside: false` → `mode: center`\n  - `toc: false` → `mode: wide`\n  - `redirect` → a redirect\n  - `bottom`, `header`, `fluid`, `main` → drop\n- **Components** (Nuxt Elements, left as written and reported):\n  - `::alert{type}` → that directive (`primary` → `tip`)\n  - `::callout{type}` with `#summary`/`#content` → the directive holding the summary, then `<Expandable title=\"Details\">` around the content\n  - `::list{type}` → a plain list\n  - `::badge{type}` → `<Badge>`\n  - `::button-link{href}` → a link\n  - `::terminal{content}` → a `bash` fence\n  - `::card-grid` + `::card{icon}` with `#title`/`#description` → `<CardGroup>` + `<Card>`\n  - `::block-hero` → the landing page\n  - `::code-block{label}` → a titled fence in `<CodeGroup>`\n  - `::video-player{src}` → `<video>`\n  - `::sandbox`, `::props`, `::copy-button`, `:ellipsis` → drop (report)\n- **URLs:** Nuxt Content 2 routes the same way, so check the old sitemap for trailing slashes and pin heading ids as above.\n\n## Dropped — report these\n\nReport each of these, and anything else you drop:\n\n- **Theme:**\n  - the brand color, when it was Docus's default emerald\n  - Nuxt UI overrides and theme CSS that doesn't map\n  - a custom `seo.titleTemplate`, `seo.schema`, and the TOC title\n  - logo extras, and socials with no Blume platform\n  - a forced color mode (the toggle stays)\n  - `navigation.sub: 'aside'` anchors\n  - a header button that showed only on the landing page (now site-wide)\n- **Navigation:** text-order sorting (`10.` before `2.`), and pages that sorted after a folder.\n- **Frontmatter:**\n  - `links` (now `related` cards)\n  - `sitemap: false`, `toc`, `layout`, and other unknown keys\n  - derived descriptions\n- **Components:**\n  - callout `to` links, callout colors (mapped to the nearest type; `<Callout color>` takes a CSS color when a tint matters), and card colors\n  - prompt icons and Windsurf/Claude actions\n  - the `code-tree` file browser and `code-preview` live rendering\n  - Nuxt UI landing decorations\n  - custom and module components\n  - brand icons with no SVG\n- **Server features:**\n  - the site's own MCP tools and prompts\n  - server routes and `@vercel/speed-insights`\n  - the assistant, if not kept\n  - old `/.well-known/skills/` file URLs, and pages whose prose describes any of these\n- **Anchors:** old ids that were already broken.\n","isInternal":false,"tokens":7574,"sizeBytes":26928},{"name":"docusaurus.md","path":".agents/skills/blume-migrate/references/docusaurus.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/docusaurus.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Docusaurus → Blume\n\nDocusaurus is a React site; navigation comes from `sidebars.js` (explicit or autogenerated) plus per-folder `_category_.json`. Admonitions are already directive syntax, which eases the content transform — **but note the source major version**: v2 (MDX v1) and v3 (MDX v3) have different content hazards, called out below.\n\n## Detect\n\n- **`docusaurus.config.{js,ts}`** at the root; a **`sidebars.{js,ts}`** file and per-folder **`_category_.json`** files.\n- Content under **`docs/`** (often plus `blog/`, `src/pages/`, `static/`, `versioned_docs/`); `@docusaurus/*` deps.\n- v2 vs v3: check the `@docusaurus/core` major in `package.json`.\n\n## Config: `docusaurus.config` → `blume.config.ts`\n\nRead `themeConfig`, `presets`, and `plugins`:\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` | `title` |\n| `tagline` | `description` |\n| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |\n| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |\n| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead (it renders in the footer) |\n| `themeConfig.navbar.items` (a `className`-styled button — \"Get started\", \"Sign up\") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |\n| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `\"system\"`) |\n| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |\n| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |\n| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md — GitHub Pages, a common Docusaurus host, is not one of them); `baseUrl` (when not `/`) → `deployment: { base: \"/…\" }`, or the `base` option of a host adapter (`vercel({ base })`) when the site also needs one |\n| preset `docs.routeBasePath` — **including the default!** | Docusaurus serves docs at **`/docs/…` by default**; the \"map only declared fields\" rule does **not** apply here because the _URLs_ are load-bearing. Either keep them with top-level **`basePath: \"/docs\"`** (invisible to the sidebar), or intentionally move to root and emit a `redirects` entry per page. Decide explicitly and say which. (`routeBasePath: '/'` = docs-only mode — nothing to do.) |\n| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`; **an origin other than `https://github.com` → `github.host`** — a GitHub Enterprise repo's edit links and header mark point at the public site without it) |\n| `themeConfig.footer` | `footer.links`, one row of `{ label, href }`: every column's items flatten into it in order (an item's `to`, a route, is its `href`); `copyright` (HTML) → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), keep a `${new Date().getFullYear()}` template literal as written, and drop a \"Built with Docusaurus.\" credit; a link inside it loses its URL (report, or add it to `footer.links`); a translated one in `i18n/<locale>/docusaurus-theme-classic/footer.json` → a locale map; column `title`s, `logo`, and `style` drop (report), or keep the whole footer as a Footer override (`defineComponents` layout slot) |\n| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |\n| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |\n| `themeConfig.algolia` | drop — Blume ships built-in search (Orama); remove the Algolia dep. To keep the index, `search: algolia({ appId, apiKey, indexName })` from `blume/search` (the DocSearch `apiKey` is the search-only key) |\n| `@docusaurus/plugin-client-redirects` | **static `redirects: [{from, to}]` arrays convert 1:1** to Blume `redirects` (a `from` array = one entry per item); only `createRedirects` functions are truly dynamic → host rules |\n| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |\n| OpenAPI doc plugins (`docusaurus-plugin-openapi-docs`, `redocusaurus`) | delete the plugin **and its generated pages** — add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list; `redocusaurus` `specs[].route` → the adapter's `route`, one `openapi()` entry (or source) per spec (see SKILL.md \"OpenAPI\") |\n| GraphQL doc generators (`@graphql-markdown/docusaurus`, `@edno/docusaurus2-graphql-doc-generator`) | delete the plugin **and its generated pages** — add `graphql({ spec, endpoint })` (from `blume/reference`) to the top-level `reference` list instead (see SKILL.md \"GraphQL\") |\n| `remark-math` + `rehype-katex` | delete — `$$…$$` renders in `.mdx` with no config (no `markdown.math` field exists), as a block or inline within a sentence; a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence |\n| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |\n\n## Static assets — required move\n\nEvery Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png` → `/img/foo.png`). Blume serves **`public/`**. **Move `static/*` (and any extra `staticDirectories`) into `public/`** — all root-relative references then keep resolving unchanged. Without this, every image and download 404s.\n\n## Navigation: `sidebars.js` + `_category_.json`\n\n- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.\n- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: \"group\"` (or `navigation.sidebar.display: \"group\"` once in config when every category collapses).\n- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder (its `title`/`description` from `link.title`/`link.description`) plus `directory: \"card\"` in the folder's `meta.ts`, which lists the folder's pages below it as the generated index did — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible: false` → that folder's `meta.ts` `display: \"flat\"`, so it stays a plain heading even under a global `\"group\"` mode. `className`/`customProps` → drop (report).\n- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.\n\n## Versioned docs\n\nDocusaurus `versioned_docs/version-X/` + `versions.json` (+ `versioned_sidebars/`) → **recommend migrating the latest released version only**. Mind the URL scheme: by default the **latest release** serves at `/docs/` and the work-in-progress `docs/` folder serves at `/docs/next` (`lastVersion: 'current'` flips this) — pick the folder that matches what users see at `/docs/`. If older versions must stay, use Blume's native versioning rather than a hand-built `navigation.selectors` dropdown: move each `versioned_docs/version-X/` into a top-level folder under `content.root` named for its id, and list that id in `versions.archived` (newest first), with `versions.current` labeling the live tree. Ids must start with a letter, so `version-1.0/` becomes `v1.0/`. Blume then adds the version switcher, the old-version notice, version-scoped search, and canonicals to the latest itself. A version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as ordinary current content. Snapshot routes become `/<id>/…`, so rewrite root-absolute links inside each snapshot to stay in it (`/guides/x` → `/v1.0/guides/x`) and add `redirects` from the old version URLs. Full reference: `docs/content/versioning.mdx` in the installed package.\n\n## Blog\n\nA Docusaurus `blog/` → Blume `type: blog` pages. **Dates come from filenames/folders** (`2024-01-31-foo.md`) — extract each into `date` frontmatter and strip the date from the filename (the old dated URLs `/blog/2024/01/31/foo` need `redirects`). Strip `<!-- truncate -->` / `{/* truncate */}` markers. `authors.yml` refs → inline author objects in each post's `authors` frontmatter. RSS stays at `/blog/rss.xml` on both sides. Blume generates **no** blog index, tag, author, or archive pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag, author, and archive pages as dropped.\n\n## Content & components\n\n- **`.md` vs `.mdx` — both majors need renames, for opposite reasons.** Blume parses `.md` as plain Markdown: no directives, no JSX, no `$$` math, no mermaid/package-install fences. **v3** treats `.md` as MDX (so a `.md` with imports/JSX/`{}` renders them as literal text in Blume); **v2** content is looser MDX v1. Rule: **rename any `.md` that contains admonitions, JSX, imports, or math to `.mdx`** — for typical Docusaurus repos that is most files.\n- **Admonitions are directives — but check the version.** v3: `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger` pass through; `:::caution` → `:::warning` (or rely on Blume's alias); titles `:::note[Title]` work. **v2:** titles are space-separated (`:::note Your Title`) — rewrite to brackets or the title is lost (`BLUME_DIRECTIVE_OPENING_TEXT` names each one); and v2's `:::warning` rendered **red/danger** — audit whether it should become `:::danger`.\n- **Tabs:** `<Tabs>`/`<TabItem label=\"…\" value=\"…\">` → `<Tabs>`/`<Tab title=\"…\">`. Drop `groupId`/`queryString`/`value`; strip the `@theme/Tabs` imports.\n- **Theme JSX in content:** `<DocCardList/>` (standard on category index pages) → delete it and set `directory: \"card\"` in that folder's `meta.ts`: Blume then lists the folder's pages as cards below the `index` page's content and keeps the list current (nested folders inherit the setting; give one `directory: \"none\"` to opt out). A hand-written `CardGroup` is only for a curated subset; `<TOCInline/>` → drop (report); `<CodeBlock>` JSX → a fenced code block; `<Admonition>` → the matching directive; `<details>`/`<summary>` → `<Accordion>`/`<AccordionItem>` or leave as raw HTML.\n- **`@theme/*` / `@site/*` imports** — strip `@theme/*` (Blume injects components globally); rewrite `@site/` asset/module paths to `/public` URLs or inline. **MDX partials** (`_partial.mdx` imports) → inline the partial's body (Blume's default `**/_*` exclude already hides the partial files themselves).\n- **Code blocks:** `title=\"file.js\"` → works as-is; `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left); **magic comments** (`// highlight-next-line`, `highlight-start`/`end`) → `{ranges}` or `// [!code highlight]` — unconverted they ship as literal comments in every sample; ` ```bash npm2yarn ` → ` ```package-install `.\n- **MDX v1 (v2 sources) pitfalls:** unescaped `<`/`{` in prose, HTML comments `<!-- -->` (→ `{/* */}`), string `style=\"…\"` attributes (→ objects). Fix as build errors surface.\n\n## Frontmatter\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` / `description` | pass through |\n| `id` | **not a safe drop** — Docusaurus builds a doc's URL from its ID, so `id: part1` in `guides/hello.md` serves `/docs/guides/part1`. Where the `id` differs from the filename (and no `slug` is set), replace it with `slug: guides/part1` (the folder path plus the id) or rename the file to match; otherwise drop it. Sidebar and `link.type: 'doc'` references name docs by this ID too |\n| `slug` | `slug` as a **full path from the content root**: Blume's `slug` replaces the page's whole route, while Docusaurus resolves a relative slug (no leading `/`) against the doc's folder. So `guides/intro.md` with `slug: start` → `slug: guides/start`; an absolute slug (`/start`) is already a full path |\n| `sidebar_label` | `sidebar.label` |\n| `sidebar_position` | `sidebar.order` |\n| `unlisted` | `hidden: true` + `noindex: true` |\n| `last_update.date` | `lastModified` |\n| `draft` | `draft` |\n| `tags` | `search.tags` (or drop if only used for the blog tag index) |\n| `image` | `seo.image` |\n| `keywords` | drop → use `seo` |\n| `sidebar_class_name` / `sidebar_custom_props` / `pagination_next` / `pagination_prev` / `pagination_label` / `hide_title` / `hide_table_of_contents` / `toc_min_heading_level` / `toc_max_heading_level` / `displayed_sidebar` / `custom_edit_url` | drop (report) |\n\nRemove any duplicated H1 in the body — `title` renders the H1.\n\n## Icons\n\nDocusaurus has no built-in icon-name system (icons are usually inline SVG/emoji/CSS). Where you add sidebar/tab icons, use Lucide names. Convert any FontAwesome/react-icons usage in content to Lucide `<Icon>` or drop it.\n\n## Teardown\n\nRemove `@docusaurus/*` and Algolia deps; delete `docusaurus.config.*`, `sidebars.*`, `babel.config.js`, and `src/` theme customizations (after the `src/pages/` inventory above; re-implement what matters via `defineComponents` layout slots or `blume eject`). Repoint `start`/`build`/`serve` scripts → `blume dev`/`blume build`/`blume preview`; add `blume`.\n\n## Dropped — report these\n\nCustom/swizzled theme components (layout slots or `blume eject`), the footer's logo and HTML items (and any link inside its copyright), the blog's generated tag, author, and archive pages, Algolia config, `sidebar_custom_props` and the other dropped frontmatter keys, `createRedirects` functions (→ host rules), React pages under `src/pages/`, `<TOCInline>`, per-category `className`/`customProps`, and any `@theme/*` component with no Blume equivalent.\n","isInternal":false,"tokens":4040,"sizeBytes":14907},{"name":"fern.md","path":".agents/skills/blume-migrate/references/fern.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/fern.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Fern Docs → Blume\n\nFern Docs is a hosted platform driven by a `fern/` folder: `docs.yml` declares the **entire** navigation (tabs, sections, pages, the API Reference, changelogs, versions, products), pages are MDX files anywhere under `fern/`, and the API Reference is generated from an OpenAPI/AsyncAPI spec or a **Fern Definition**. Two things set it apart from the other sources:\n\n- **The same `fern/` folder usually drives SDK generation too** (`generators.yml`, `definition/` or `apis/<name>/`). Only the docs move. Never delete or rewrite the API definition, a `generators.yml`, or `fern.config.json` (see [Teardown](#teardown-keep-sdk-generation)).\n- **URLs come from `docs.yml`, not from file paths.** A page's URL joins slugs from its tab, sections, and page name, and a frontmatter `slug` replaces most of that. Files sit wherever the team put them (`pages/get-started/welcome.mdx` can serve `/welcome`).\n\nScope: checked against Fern CLI 5.44.8 and an end-to-end migration of agentmail-to/agentmail-docs (115 pages, a Fern Definition with 149 endpoints and 8 webhooks, 270 old URLs). Versions, products, tab variants, translations, `folder:` entries, and OpenAPI (rather than Fern Definition) inputs weren't in that run: the guidance for them below is untested.\n\n## Detect\n\n- **`fern/docs.yml` beside `fern/fern.config.json`** (run from the repo root), or `docs.yml` + `fern.config.json` in the working directory (run from inside `fern/`). Monorepos nest it (`docs/fern/docs.yml`).\n- `fern.config.json` **without** a `docs.yml` is an SDK-only Fern project: the docs live elsewhere (often a Mintlify `docs.json`), so use that source's reference and leave `fern/` alone.\n- A stale `fern/docs.yml` can outlive a move off Fern. If the repo also carries another docs config, check which one the live site runs: Fern pages answer with an `x-fern-renderer` header (`curl -sI https://<docs-domain>/ | grep -i x-fern`).\n\n## Run the codemod first\n\nThe bundled codemod does the mechanical half: URL computation and file placement, `meta.ts`, frontmatter, the common components, assets, and every redirect. Steps, in order:\n\n1. **Save the old URL list.** Fetch `https://<docs-domain>/sitemap.xml` and keep only the paths, one per line, in `old-urls.txt` (a Fern sitemap can name a different host than the one you fetched). Hidden pages aren't in it; the codemod computes those from `docs.yml`. Run the CORS probe under [Try it](#layout-summary-snippets-try-it) while you're there.\n2. **Dry run**, from the folder that holds `fern/` (where `blume.config.ts` goes):\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt\n   ```\n\n   It prints a sitemap diff (`0 computed URL(s) not in it, 0 of its URLs not computed` is the goal; otherwise fix those pages by hand with [Fern's URL rules](#ferns-url-rules) and report them), the `navigation.tabs` entries for the config, which APIs are Fern Definitions, a TODO list of judgment calls, and the icons it couldn't map. `--out` sets the content root (default `docs`), `--public` the public folder, `--fern` the Fern folder. It **stops** on `versions` or `products`: migrate those by hand.\n\n3. **Apply** with `--write`. It moves every published page to a path that reproduces its old URL, writes frontmatter and `meta.ts` files, converts components, moves the assets pages use (and the logo and favicon) into `public/`, and saves `fern-migration.json`. A YAML or JSON file a page links to (a spec offered for download) is copied, not moved, since SDK generation may read it. It's idempotent: a rerun skips moved pages.\n4. **Write `blume.config.ts`** from [Config](#config-docsyml--blumeconfigts), with the printed tabs and `logo.href` set to the page `/` now redirects to.\n5. **Set up the API reference** ([API reference](#api-reference)): export a Fern Definition, write the overlay (tag names, webhooks, idempotency headers), add `openapi()`.\n6. **Build, then list the operation routes**:\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist > routes.json\n   npx fern-api@<version> export --api <api-name> spec.json   # JSON because the path ends in .json\n   ```\n\n   The join needs a JSON copy of the spec the build rendered: for a Fern Definition, that export (a scratch file, not the committed spec); for a YAML OpenAPI input, `npx @redocly/cli bundle <spec>.yaml --output spec.json`.\n\n7. **Join endpoints and write the redirects**:\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt          # dry run\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt --write\n   ```\n\n   One `--spec <route>=<file>` per API. It maps every old endpoint, webhook, and group URL to its Blume route, translates `docs.yml` redirects, adds the content-move, changelog, feed, and `/` redirects, rewrites content links that point at old URLs, and checks every old URL is served or redirected (it must print `every old URL is served or redirected`). `--write` saves `fern-redirects.json`; import it:\n\n   ```ts\n   import fernRedirects from \"./fern-redirects.json\" with { type: \"json\" };\n   // in defineConfig:\n   redirects: [...fernRedirects],\n   ```\n\n   Fix a redirect it flagged by filtering that entry out and adding your own after the spread.\n\n8. **Work the TODO list**, then `blume build`, `blume validate --strict`, `blume audit --only redirects`, SKILL.md's heading-anchor check (`pin-heading-ids.mjs`), and `fern check` after the [teardown](#teardown-keep-sdk-generation).\n\nWhat it leaves to you: the config, the overlay, the TODO list, the components it names, links already broken on Fern, CI, and contributor docs. It never touches `generators.yml`, the definition, or a spec.\n\n## Inventory\n\nRead `docs.yml` first, then every file it points at: `redirects` (a list, or YAML files that each hold a `redirects:` list), `tabs.<key>.changelog`, every `path:` in `navigation`, `css`, `js`. **Every path in a Fern YAML file is relative to that YAML file.**\n\n- **Unpublished drafts.** Fern builds only the files `docs.yml` references plus each changelog folder; any other `.md`/`.mdx` under `fern/` is a draft nobody sees. The codemod leaves them in place and lists them. Blume would publish them: delete them or move them to a `_`-prefixed folder (`_drafts/`), and report the list.\n- **SDK generation.** Every `generators.yml` and its `groups` stay exactly as they are.\n- **Second instances.** Each `instances[]` entry is a deployment of the same docs (a second domain, a subpath). One Blume build serves one; report the rest (see [Deploy](#urls-redirects-and-deploy)).\n\n**Where the Blume project goes.** If the repo root has a `package.json`, put `blume.config.ts` there with content in `docs/` (the default root). If it has no JavaScript toolchain (a Python or Go repo whose docs were only `fern/`), make a self-contained docs package (`docs/` holding `package.json`, `blume.config.ts`, and `content.root: \"content\"`), run the codemod from there with `--fern ../fern --out content`, and say which you chose. Don't make `fern/` itself the content root: it keeps holding the SDK config. See `references/monorepo.md` for workspaces.\n\n## Config: `docs.yml` → `blume.config.ts`\n\nMap only what's set.\n\n| Fern `docs.yml` | Blume | Notes |\n| --- | --- | --- |\n| `title` | `title` | Fern shows it only in the browser tab; Blume also shows it beside the logo (see `logo`) |\n| `logo` (`{ light, dark, href, height, right-text }`) | `logo: { image: { light, dark, alt }, href, text }` | the codemod moves the files into `public/`. Fern shows the image alone unless `right-text` is set, so set **`text: right-text`, or `text: \"\"`**. A local monochrome SVG can collapse to `logo: \"/logo.svg\"` with `currentColor` fills (SKILL.md). Set `href` to the first page when nothing serves `/`. `height` drops |\n| `favicon` | **drop the field** | the codemod moves the file to `public/favicon.<ext>`; Blume detects it by filename |\n| `colors.accent-primary` (or `accentPrimary`), a string or `{ light, dark }` | `theme.accent` | same shape |\n| `colors.background` | `theme.background` | same shape |\n| `colors.*`, `layout.*`, `settings.*` (others), `theme.*`, `global-theme`, `page-actions`, `experimental`, `check`, `ai-examples`, `libraries`, `roles` | **drop** (report) | `global-theme` merged a registry theme at publish: read its values off the live site |\n| `typography.bodyFont` / `headingsFont` / `codeFont` | `theme.fonts.body` / `display` / `mono` | Fern fonts are files in the repo (`path`, or `paths` with `weight`/`style`): each role takes `{ name, variants: [{ src, weight, style }] }` |\n| `navbar-links` `type: filled` / `primary` | `navigation.cta` (`{ label: text, href }`) | one only; extra filled links go to `actions`. `url` is Fern's old name for `href` |\n| `navbar-links` `type: minimal` / `outlined` / `secondary` | `navigation.actions` (`[{ label: text, href }]`) | text only: link icons drop. A `mailto:` link works |\n| `navbar-links` `type: github` / `dropdown` | `github: { owner, repo }` / **no equivalent** | flatten a dropdown into `actions` (report) |\n| `footer-links` | `footer.socials` | keys carry over except `twitter` → `x`, `hackernews` → `hacker-news` |\n| `footer` / `header` (React files) | **drop** (report) | or a layout slot in `components.ts` |\n| `instances[].custom-domain` | `deployment.site` (`https://…`) | only when the target host isn't Vercel or Netlify (SKILL.md). **A domain with a path** (`acme.com/docs`) is a reverse-proxied subpath → `deployment: { base: \"/docs\" }`. **Several instances** → one Blume build; keep the canonical one (`metadata.canonical-host` names it when set) and report the rest |\n| `instances[].edit-this-page.github` (`{ owner, repo, branch }`) | `github: { owner, repo, branch, dir }` | `dir` = path from the repo root to the folder holding `blume.config.ts` |\n| `metadata.canonical-host` | `deployment.site` | same host rule as `custom-domain`; a path in it is the `base` above |\n| `metadata.og:description` / `twitter:site` | `description` / `seo.x.handle` | other `og:*`/`twitter:*` drop: Blume writes them |\n| `announcement.message` | `banner` | a string, or `{ content, link: { text, href } }` |\n| `analytics.*` | the matching factory from `blume/analytics` (`posthog`, `googleAnalytics`, `googleTagManager`, `segment`), or `script()` | a `${VAR}` value → `process.env.VAR` |\n| `js` (a path, `{ path, strategy }`, or `{ url, strategy }`) | `script({ src \\| content, strategy })` per tracking script | a third-party loader ports; code that reads or restyles Fern's DOM (`.fern-*`) doesn't (report it). Fern's strategies (`beforeInteractive`, `afterInteractive`, `lazyOnload`) aren't Blume's (`async`, `defer`): pick one or leave it out. A `<link rel=\"help\" href=\"/llms.txt\">` injection drops: Blume publishes `llms.txt` |\n| `css` | a root `theme.css` | `.fern-*` selectors match nothing in Blume; report what you drop |\n| `default-language`, `settings.http-snippets` | `codeSamples` on `openapi()` | that language first; a list → that list; `false` → `false` |\n| `settings.disable-search` | `search: false` |  |\n| `layout.hide-feedback` | `feedback: false` |  |\n| `ai-search` / `ai-chat` (Ask Fern) | **drop** (report) | Blume's assistant is opt-in and self-hosted (`ai.assistant`, a provider key, server output); offer it, don't enable it silently |\n| `agents.*` | **drop**, or `agents.llmsTxt.details` for directive text | Blume generates `llms.txt` and `robots.txt` |\n\n## Navigation\n\n### Fern's URL rules\n\nThe codemod applies these; you need them for its sitemap diff and anything it doesn't handle. Each level contributes a slug, joined outermost first: **product → version → tab → sections → page**.\n\n- **Each slug is the item's `slug:` if set, else lodash `kebabCase` of its display name**: tab `display-name` (not the tab's key), `section:` title, `page:` name. `kebabCase` lowercases, drops punctuation, and splits camelCase and letter/digit boundaries: `API Reference` → `api-reference`, `AgentMail` → `agent-mail`, `SOC 2` → `soc-2`, `v3 (Latest)` → `v-3-latest`.\n- **`skip-slug: true`** on a tab, section, or api entry drops that level from the URL but keeps it in the sidebar. A changelog tab ignores it.\n- **A frontmatter `slug` is the page's whole URL below any product/version prefix**: it replaces the tab and section slugs too. A `docs.yml` `slug:` replaces only that item's own segment.\n- **An `api` entry's base** is its `summary` page's frontmatter `slug` when set; otherwise its `slug` (or `kebabCase(title)`) under the tab.\n- **A changelog** serves its index at its slug and **each entry at `<changelog>/<YYYY>/<M>/<D>`** (no zero padding), titled with the date (`September 5, 2026`). Two entry files dated the same day share one URL: Fern merged them into one page.\n- **`/`** answers with a 307 to the first page when no landing page is set. **API group paths** (`/api-reference/inboxes/threads`) answer with a 307 to the group's first endpoint and aren't in the sitemap, but content links to them.\n- **The first version is the default and has no version segment**; every other version is prefixed with its slug. Products always carry their slug.\n\n### What the codemod builds\n\n- **One folder per tab** at the tab's URL prefix. The tab whose pages share no single prefix (a `skip-slug` tab, or one whose pages all carry root-level frontmatter slugs) becomes the content root, with a tab at `path: \"/\"`. Only one tab can be the root; the codemod reports a second one, which must move under a prefix (route change, redirects).\n- **A section** whose pages share a URL segment → a real folder; one that adds no segment → a `(group)` folder. A section's `path` (its overview page) → that folder's `index.mdx`.\n- **A page whose URL departs from its folder** (a frontmatter `slug`) stays in its sidebar folder with `slug` set, since Blume places a page in the sidebar by file path, not route.\n- **A file `docs.yml` lists twice** is published once, at its first listing; the other URL redirects there (reported).\n- **`meta.ts`** per folder: `pages` in `docs.yml` order, the section title and Lucide icon, and a collapsible section (`collapsed`, `collapsible`, `collapsed-by-default`) → `display: \"group\"`. Blume lists loose pages above subgroups, so a section that interleaves pages and subsections renders its pages first: report any such reorder.\n- **Labels.** Fern labels a page in the sidebar with its `page:` name (or `sidebar-title`) and titles it with frontmatter `title`. So: no `title` → `title: <page name>` (or an H1 that opens the body, which it removes); a different `title` → `sidebar.label: <page name>`. The api **summary** page keeps its `title` with no label (its page name usually repeats the tab label).\n- **`hidden: true`** → `hidden: true`, `seo: { noindex: true }`, and `search: { exclude: true }`: Fern keeps hidden pages reachable but out of search and indexing.\n- **The root URL.** Nothing serves `/` under a root tab, so it redirects `/` to the first page (307), as Fern did; set `logo.href` to that page. The tab at `/` links to the sidebar's first page on its own. A root `index.mdx` would move the landing page to `/`; do that only if the user wants `/` as the landing URL.\n\nBy hand: `availability` (`beta`, `deprecated`, …) → `sidebar.badge` (`Beta`) or `deprecated: true`; `- link:` items → `navigation.featured` (`{ label, href, icon? }`) or an explicit `navigation.sidebar` link (report which); a hidden **tab** (its printed line ends `(hidden on Fern)`) → leave it out of `navigation.tabs`; the codemod already marked its pages hidden.\n\n### Tabs\n\nThe codemod prints one `navigation.tabs` entry per tab (`{ label: display-name, path, icon }`). Keep them as header tabs.\n\n- An **`href` tab** (Blog, GitHub, a dashboard) has no content → `navigation.actions` or `navigation.featured`; a GitHub tab → `github`.\n- **Tab `variants`** (several sidebars under one tab): the codemod reports them. One subfolder per variant and a tab with `items` (`{ label, path }` per variant); variant pages move, so add redirects and report the approximation.\n\n### Changelog\n\nThe codemod moves each entry to `changelog/<YYYY-MM-DD>[-suffix].mdx` (`MM-DD-YYYY` and `MM-DD-YY` names are renamed, so entries sort by date and a two-digit year's month isn't read as an ordering prefix. Fern parsed those in the publishing machine's time zone, so one can sit a day off: the sitemap diff shows it) with `type: changelog`, `date`, and a `title`: the frontmatter one, the date as Fern showed it, or the file suffix sentence-cased (`agentid-sign-in-keys` → \"Agentid sign in keys\"). **Review every suffix title** (`AgentID sign-in keys`); the TODO list names them. `tags` → `changelog.category` (the first) and `search.tags` (all).\n\n- **Same-day entries.** The day's Fern URL redirects to the unsuffixed entry, or else the first by file name. Fern showed both on one page: consider linking the second from the first, and report it.\n- `overview.mdx` (or `index.mdx`, `summary.mdx`) → its text goes into `changelog: { title, description }` in `blume.config.ts`, since an `index.mdx` would replace Blume's generated `/changelog` index (a list, where Fern's index rendered whole entries). Its `layout` and `authors` drop.\n- A non-date file (`TEMPLATE.mdx`) was never published: move it out or `_`-prefix it, and update it to Blume's format if contributors copy it.\n- The feeds `/<changelog>.rss` (`.atom`, `.json`) redirect to `/changelog/rss.xml`, which needs a site URL.\n- For a public GitHub repo, offer `githubReleases()` as SKILL.md \"Changelogs\" says; keep the files when the user says the entries are curated rather than release notes.\n\n### Versions and products (by hand, untested)\n\n- **`versions`** → Blume's native versioning (`docs/content/versioning.mdx` in the installed package). The first Fern version is the live tree at the content root; every other version → a folder named by its slug, listed in `versions.archived`. Place each version file's `navigation` with the same rules; a `ref:` version lives in git, so check it out. Per-version API references → one `openapi()` each, with its own `route`.\n- **`products`** → one folder per product plus `navigation.selectors: [{ kind: \"product\", label, items }]`.\n- `<If versions products>` → keep each folder's matching branch.\n\n## Content and components\n\nThe codemod writes every page as `.mdx` and converts:\n\n- **Callouts.** `<Note>`, `<Tip>`, `<Info>`, `<Warning>`, `<Success>`/`<Check>`, `<Error>`, `<Launch>`, and `<Callout intent>` (no intent → `info`, as Fern rendered it). A top-level one becomes a `:::type[Title]` directive (dedented). One whose icon maps to Lucide (a directive can't set one), or one nested in a component, becomes `<Callout type title icon>`; an icon with no Lucide equivalent drops (reported).\n- **Code groups.** `<CodeBlocks>` → `<CodeGroup>`; every untitled fence inside a group gets Fern's language title (`title=\"Python\"`, `title=\"cURL\"`). Blume labels an untitled grouped fence by its language anyway, so the title only keeps Fern's spelling. `<CodeBlock title>` wrappers are unwrapped onto the fence: Blume's `<CodeBlock>` renders a wrapped fence, but not the wrapper's `title`.\n- **Fence metas.** Multi-word bare titles are quoted (Fern and Blume both read every word of ` ```js Snippet with title` as the title), `filename=` → `title=` and `wordWrap` → `wrap` (Blume warns `BLUME_CODE_FENCE_OPTION` on the Fern spellings), `env` → `dotenv` (Shiki has no `env`: `BLUME_UNKNOWN_CODE_LANGUAGE`).\n- **Cards and accordions.** `<Cards>` → `<CardGroup>`, card icons → Lucide, Fern-only card props drop. `<AccordionGroup>`/`<Accordion>` → `<Accordion>`/`<AccordionItem>`; a lone `<Accordion>` is wrapped in one.\n- **`<Icon>`**: icon → Lucide, `size` × 4 (Fern counts in 4px units). **`<Frame>`**: `background` drops. **`<Tab language>`**: `language` drops (Blume syncs same-titled tabs).\n- **`<ParamField path>`** → `<ParamField name>`: Fern's `path` is the field's name, and Blume's `path=` would mark it a path parameter. `toc` drops. Nested fields in an `<Indent>` (reported) → an `<Expandable title=\"properties\">` in the parent field's body.\n- **Snippets.** `<Markdown src=\"/snippets/x.mdx\" planName=\"x\" />` → `<include planName=\"x\">/_snippets/x.mdx</include>` on a line of its own, with the snippet moved to `<content-root>/_snippets/`. Its `{{planName}}` placeholders carry over as written; Blume reads an include prop only when its name starts with a lowercase letter, so rename any that don't.\n- **Embeds and comments.** A YouTube iframe → `<YouTube id>` (a playlist embed → `<YouTube url>`); `allowfullscreen`/`frameborder` → `allowFullScreen`/`frameBorder` (MDX drops the lowercase forms silently); `<!-- -->` → `{/* */}`.\n- **Assets.** `/assets/x.png` and `../x.png` → moved into `public/` at their path from `fern/` and linked root-absolute. A YAML or JSON file is copied instead (SDK generation may read it; the copy goes stale when it changes). An asset no page or config references stays put: move or delete it.\n- **Links.** Absolute links to the docs' own domain → root-relative; links to moved pages, endpoints, and Fern's `api:METHOD/path` syntax → the new routes (the `endpoints` step).\n\n**Reported for you to convert** (a leftover Fern tag gets a `BLUME_UNKNOWN_COMPONENT` warning, or crashes the page):\n\n| Fern | Blume |\n| --- | --- |\n| `<EndpointRequestSnippet endpoint=\"POST /x\">`, `<EndpointResponseSnippet>` | a hand-written fenced sample plus a link to the operation page: Blume can't embed an operation's sample in prose |\n| `<EndpointSchemaSnippet>`, `<Schema>`, `<SchemaSnippet>`, `<WebhookPayloadSnippet>`, `<RunnableEndpoint>` | a link to the operation page; a small schema can become a `TypeTable` |\n| `<Badge intent outlined>`, `<Availability type>` | `<Badge variant stroke>` (`success`, `warning`, `error` → `danger`, `tip` → `accent`); an availability that labels the page → `sidebar.badge` |\n| `<Code src title>` | `<include meta='title=\"…\"' lang=\"…\">./path</include>` with the file inside the content root |\n| `<Files>`/`<Folder>`/`<File>` | `<Tree>`/`<Tree.Folder>`/`<Tree.File>` |\n| `<Versions>`/`<Version title>` | `<Tabs>`/`<Tab title>` |\n| `<If>` (roles), `<Feature flag>` | the public or default content; ask before dropping role-gated text |\n| `<Button href>`, `<Download src>`, `<Copy>` | a link (or `<Card href>`), a link to the file in `public/`, inline code |\n| `<Aside>`, `<Indent>`, `<StickyTable>`/`<SearchableTable>`/`<PaginatedTable>` | the content inside |\n| `<Prompt title>`, `<ScrollWalkthrough>` | `<Prompt description>`; `<Steps>` with a code block per step |\n| `<ChangelogTags>`, `<Template>` | delete (report) |\n\nNot reported, but check: headings inside `<Steps>` (Fern makes each a step) → one `<Step title>` each; `<Anchor id>` → `<a id=\"x\"></a>`; `<llms-only>`/`<llms-ignore>` → `<Visibility for=\"agents\">`/`<Visibility for=\"web\">`; `maxLines` → `expandable`; inline `$…$` math → `$$…$$`; custom React components → an island or a `components.ts` override. **Unclosed fences** (Fern rendered an empty block): the codemod closes them at the end of the file and reports them; delete the stray fence.\n\n### Frontmatter\n\nFern ignores unknown keys; Blume's schema is strict. The codemod maps `title`, `sidebar-title` → `sidebar.label`, `subtitle` → `description` (Blume renders `description` under the title), `description` → `seo.description` when `subtitle` took `description`, `headline` → `seo.title`, `slug`, `noindex` → `seo.noindex`, and the `docs.yml` `icon`. It reports every other key; map these by hand:\n\n| Fern | Blume |\n| --- | --- |\n| `canonical-url` | `seo.canonical` (absolute) |\n| `image`, `og:image` | `seo.image` |\n| `keywords` | `search.keywords` (a list) |\n| `last-updated` | drop it everywhere and set `lastModified: \"git\"` site-wide; retire any workflow that stamps it, since the key now fails the build |\n| `layout` | `mode`: `overview`/`reference` → `wide`, `page` → `center`, `custom` → `custom`, `guide` → drop |\n| `hide-nav-links: true` | `pagination: false` |\n| `availability` | `sidebar.badge`, or `deprecated: true` |\n| `position` (orders pages in a `- folder:` entry) | `sidebar.order` |\n| `hide-toc`, `force-toc`, `max-toc-depth`, `hide-feedback`, `hide-page-actions`, `no-image-zoom`, `breadcrumb`, `search-metadata`, `edit-this-page-url`, `logo`, `nofollow`, `jsonld:breadcrumb`, `og:*`, `twitter:*`, `excerpt`, `tags` | drop (report) |\n| keys Fern never read (`sidebar_position`, `lastUpdated`) | drop |\n\n### Icons\n\nFern uses Font Awesome; Blume is Lucide-only. The codemod normalizes Fern's spellings (`fa-solid fa-bolt`, `solid bolt`, bare `bolt`), maps them to Lucide names Blume bundles, and reports the rest. Most brand icons (`discord`, `google`, `aws`, `python`, `node-js`, `x-twitter`) have no Lucide equivalent: they drop. An image path on a component (`icon=\"./icons/x.svg\"`) → `public/` and a root-absolute path; one in `docs.yml` is reported for you to move. For icons you write yourself (config, hand conversions), check each name from the folder holding `blume.config.ts`; an unknown icon renders nothing:\n\n```bash\nnode -e \"const p=require.resolve('@iconify-json/lucide/icons.json',{paths:[require('path').dirname(require.resolve('blume/package.json'))]});const j=require(p);for(const n of process.argv.slice(1))console.log(n,n in j.icons||n in (j.aliases||{}))\" zap mail-check discord\n```\n\n## API reference\n\nEach `- api:` entry → one `openapi()` (or `asyncapi()`/`graphql()`) source from `blume/reference`, with **`route` set to the api entry's old base URL** so the overview keeps its address, and a tab at that route. Find the spec each entry renders:\n\n- `specs:` on the api entry → those files (`type: openapi | asyncapi | graphql`).\n- Otherwise `api-name: <name>` → `fern/apis/<name>/`, or `fern/` with one API. There, `generators.yml` `api.specs` lists OpenAPI/AsyncAPI files (older projects use top-level `openapi:`/`async-api:`); **a `definition/` folder is a Fern Definition**, which the codemod's dry run names.\n\n### OpenAPI and AsyncAPI inputs\n\n- **Point `spec` at the file where it lives** (SDK generation reads it too).\n- **Fern `overrides`** are Fern's deep-merge format, not OpenAPI Overlays. Most hold only `x-fern-*` SDK extensions, which Blume ignores; content an override changes for readers → an Overlay in `overlays`. Fern's own `overlays:` files carry over.\n- **`x-fern-ignore: true` operations would appear** in Blume: remove them with an overlay action (`target: $.paths.*[?@['x-fern-ignore'] == true]`, `remove: true`), likewise operations an api entry's `audiences` filtered out.\n- gRPC and OpenRPC references have no Blume renderer: report them.\n\n### Fern Definition inputs → export to OpenAPI\n\nBlume can't read a Fern Definition. Export it with the Fern CLI, which runs offline with no login:\n\n```bash\nnpx fern-api@<version from fern/fern.config.json> export --api <api-name> openapi/<api-name>.yml\n```\n\n- The output is **OpenAPI 3.0.1** (Blume upgrades it). Operation ids are `<package path>_<endpoint>` (`inboxes_threads_list`); each Fern package becomes one PascalCase tag (`InboxesThreads`); `info.title` is the API's name with an empty `version`.\n- **It drops webhooks, WebSocket channels, and `idempotency-headers`.** Restore the first and last with the overlay below; a WebSocket channel has no Blume page, so redirect its old URL to the guide that covers it and report it.\n- **A committed export.** If the repo already commits an OpenAPI generated from the definition (a `fernapi/fern-openapi` generator group writing locally, or a CI step running `fern export`), point `spec` at it, then check it isn't stale: export to a scratch path and diff. If they differ, report it and refresh it with the repo's own command (`fern generate --group <group> --local`), not a second copy. Look for workflows that trigger on that file (a release pipeline) before committing a refresh. Only when nothing produces a committed file, commit your export and add a `\"docs:spec\"` script that reruns it.\n\nDocs-only changes go in an overlay so the spec stays regenerable. The agentmail overlay, abridged:\n\n```yaml openapi/docs.overlay.yaml\noverlay: 1.1.0\ninfo: { title: Docs fixes, version: 1.0.0 }\nactions:\n  - target: $.info\n    update: { title: AgentMail API }\n  # Tags are flat in Blume: name the concatenated package tags.\n  - target: $.paths.*.*.tags[?@ == 'InboxesThreads']\n    update: Inbox Threads\n  # Fern's sidebar order.\n  - target: $\n    update:\n      tags: [{ name: Inboxes }, { name: Inbox Threads }]\n  # Webhooks the export dropped: their payload schemas survive in components.\n  - target: $\n    update:\n      x-webhooks:\n        messageReceived:\n          post:\n            summary: Message Received\n            tags: [Webhook Events]\n            requestBody:\n              content:\n                application/json:\n                  schema: { $ref: \"#/components/schemas/MessageReceivedEvent\" }\n            responses:\n              \"200\":\n                description: Return a 2xx status to acknowledge the event.\n  # One action per `idempotent: true` endpoint in the definition.\n  - target: $.paths['/v0/inboxes/{inbox_id}/messages/send'].post.parameters\n    update:\n      - name: Idempotency-Key\n        in: header\n        schema: { type: string }\n```\n\n**Restore webhooks this way** rather than redirecting their pages: one `x-webhooks` entry per webhook in the definition, keyed by its name, with a `$ref` to its payload schema. Blume upgrades `x-webhooks` to `webhooks` and gives each a page at `<route>/<tag-slug>/<kebab-name>`, which the `endpoints` step matches to the old URL. Nested packages (`Inboxes › Threads`) become sibling tags: report the lost nesting.\n\n### Layout, summary, snippets, Try it\n\n- **`summary`** (a page shown at the API's root) → the codemod places it at `<route>/introduction.mdx`, a normal page in the reference sidebar. The old base URL serves Blume's overview, which renders `info.description`.\n- **`layout`**, `alphabetized`, `flattened`, `paginated` → Blume orders tag groups by the top-level `tags` list (set it in the overlay) and operations within a tag in the spec's order (paths as listed, each path's methods as written). An endpoint order that `layout` or `alphabetized` set and the spec doesn't share → a `pages` list of operation slugs in `<route>/<tag-slug>/meta.ts`, or report it. An endpoint's `title` → its `summary` via overlay; `hidden: true` endpoints → remove them or let them show (ask).\n- **`snippets`** (SDK package names) → Fern rendered SDK calls; Blume generates HTTP-client samples. Add `x-codeSamples` through the overlay to keep SDK calls, or report the loss.\n- **Try it.** Fern's API Explorer proxies requests; Blume's playground sends them from the browser, so the API must answer CORS for the docs origin. Probe it:\n\n  ```bash\n  curl -si -X OPTIONS https://<api-host>/<path> -H 'Origin: https://<docs-domain>' -H 'Access-Control-Request-Method: GET'\n  ```\n\n  No `Access-Control-Allow-Origin` in the answer → `playground: false`, and report it; the alternative is `playground: { proxy: true }`, which needs server output and a host adapter (SKILL.md). `environments: []` → `playground: false`; `oauth: true` → Blume takes a pasted token and doesn't run the flow (report).\n\n### API URLs\n\nThe `endpoints` step computes these; check its output against them.\n\n- **Fern Definition**: the operation id split on `_`, each part kebab-cased, under the base: `inboxes_threads_list` → `/api-reference/inboxes/threads/list`.\n- **OpenAPI**: with `x-fern-sdk-method-name`, `<base>/<x-fern-sdk-group-name>/<x-fern-sdk-method-name>`; otherwise `<base>/<first tag>/<the operation id minus the tag's leading words>`, or the summary when there's no operation id. Each part kebab-cased (`v1` → `v-1`). Read from Fern's parser, not from a migrated site: the `endpoints` step's coverage check catches any URL it gets wrong.\n- **Group paths** → the overview's anchor for that tag: `/api-reference/inboxes/threads` → `/api-reference#inbox-threads` (307, as Fern's was). Content linking to a group gets the same anchor; `blume validate` checks it.\n\nBlume routes rarely match Fern's (the `endpoints` step skips any that do), and a pattern redirect over the old prefix also covers the new pages (`BLUME_REDIRECT_MATCHES_PAGE`), hence one exact redirect per endpoint. Don't shorten operation ids with an overlay: OpenAPI needs them unique across the whole spec, and shortened ones repeat (`list` in every tag), which warns `BLUME_OPENAPI_DUPLICATE_OPERATION_ID`.\n\n## URLs, redirects, and deploy\n\n- **`docs.yml` redirects** → `{ from, to, status }`: Fern's `permanent` defaults to true (308), `permanent: false` → 307; Blume's own default is 301, so the codemod writes the status. `:name` and `:name*` carry over; `:name+` and regex groups are reported (expand them into exact entries). A destination that moved is followed to its final route, and a `:slug*` pattern into the reference expands into one entry per operation. Trailing-slash duplicates merge.\n- **A destination that's neither a page nor another redirect** was already broken on Fern (the TODO list names it): point it at the intended page and report it.\n- **`blume validate --strict` finds links that already 404 on Fern.** Fix the link, don't add a redirect for it, and report the list.\n- **A subpath instance** (`acme.com/docs`): sources, destinations, and links carry the subpath; strip it, since Blume adds `base` itself. Serving both a domain and a subpath means two builds.\n\n## Teardown (keep SDK generation)\n\n**Keep**, even with no docs left in it: `fern/fern.config.json`, every `generators.yml`, the Fern Definition, every spec, override, and overlay a `generators.yml` references, and CI steps that run `fern check`, `fern generate --group <sdk>`, or `fern export` (with the `FERN_TOKEN` secret if they use it).\n\n**Remove**, once migrated: `fern/docs.yml`, redirect YAML files, the docs folders that moved (pages, `changelog/`, `snippets/`, assets, docs `components/`, `css`/`js`), CI steps and wrapper scripts that publish or preview the docs (`fern generate --docs`), and the `fern-api` dependency only if nothing else uses the CLI.\n\nThen **grep the repo for the old paths and syntax**: `fern/pages`, `fern/changelog`, `fern/snippets`, `docs.yml`, `<CodeBlocks>`, `<Note>`, `last-updated`. Contributor docs (`AGENTS.md`, `CONTRIBUTING.md`, templates) and generators that write into `fern/snippets/` need the new paths, frontmatter, and components. Run `fern check` afterwards.\n\nRepoint `package.json` scripts to `blume dev`/`build`/`preview`, add `blume`, and regenerate the lockfile (SKILL.md step 6). Tell the user what can't be committed: moving the custom domain's DNS (or the proxy behind a subpath instance) off Fern, then unpublishing the site in the Fern dashboard.\n\n## Dropped: report these\n\n- **Hosted features**: the API Explorer's proxy, login-injected credentials, and OAuth flow; Ask Fern search and chat; AI-generated examples; **Fern's docs MCP server (`/_mcp/server`)**, which has no static equivalent (Blume's MCP server is at `/mcp` and needs server output; `.md` page mirrors and `llms.txt` carry over), so update content that advertises it; the dashboard, analytics, preview links, RBAC and authenticated docs (host-level protection instead, per the deployment docs' **Private docs** section), feature flags.\n- **API reference**: SDK snippets (unless rebuilt as `x-codeSamples`), WebSocket channel pages, a `layout` or `alphabetized` endpoint order you didn't rebuild with `meta.ts`, nested sections, hidden endpoints, gRPC/OpenRPC references.\n- **Navigation and theme**: dropdown and link icons, approximated variants, sidebar reorders, moved `link` items, extra `colors`, `layout`, custom `header`/`footer`, `css` aimed at `.fern-*`, and `js` that isn't a tracking script.\n- **Content**: API embeds replaced by samples or links, code tooltips, `<ChangelogTags>`, role gating, and each dropped frontmatter key.\n- **Icons** with no Lucide equivalent, by name and where they were used.\n- **Unpublished drafts** and what was done with them.\n- **Second instances** and what replaces them.\n","isInternal":false,"tokens":9580,"sizeBytes":36462},{"name":"fumadocs.md","path":".agents/skills/blume-migrate/references/fumadocs.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/fumadocs.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Fumadocs → Blume\n\nFumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present.\n\n## Detect\n\n- Content under **`content/docs/`** (the Fumadocs convention).\n- Per-folder **`meta.json`** files.\n- `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps, and collections declared either in a `source.config.ts` (the Config API) or, in newer scaffolds, with `defineDocs` from `fumadocs-mdx/macro` inside `lib/source.ts` (no `source.config.ts` at all).\n- A `loader({ baseUrl })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`). Newer scaffolds pass a constant (`baseUrl: docsRoute`) defined in `lib/shared.ts` — resolve it there.\n\n## Config\n\nFumadocs declares little Blume needs — read the loader and the shared layout options:\n\n- **Title** — the header title: `nav.title` in `baseOptions` (`lib/layout.shared.tsx`, or `app/layout.config.tsx` in older scaffolds; often the `appName` constant from `lib/shared.ts`; extract the text if it's JSX). Without one, read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case); for a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead.\n- **Header settings** (the same `baseOptions`): `githubUrl` → `github: { owner, repo }` (the repository and edit links; when it's built from a `gitConfig` constant in `lib/shared.ts`, `{ user, repo, branch }` → `{ owner: user, repo, branch }`); `links` entries → `navigation.actions` (`{ label: text, href: url }`), except a `type: \"icon\"` link to a platform `footer.socials` knows (Discord, X, …), which goes there instead. Menus, custom items, and JSX in `nav` → report.\n- **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: \"/docs\"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `\"/\"`, serve from the site root (nothing to set).\n\nEverything else is Blume defaults.\n\n**Also read the MDX options** — remark/rehype plugins live in `source.config.ts` `mdxOptions`, or, with the macro API, in the collection options passed to `defineDocs` (a macro project may still keep a `source.config.ts` for global options):\n\n- `remark-math`/`rehype-katex` → nothing to configure: `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field), as a block or inline within a sentence. A single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence.\n- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.\n- Custom Shiki transformers/themes or other plugins → report.\n\n## Navigation: `meta.json` → `meta.ts`\n\n**Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`):\n\n| Fumadocs `meta.json` | Blume `meta.ts` |\n| --- | --- |\n| `title` | `title` |\n| `icon` | `icon` — **convert the casing** (see Icons below) |\n| `defaultOpen: false` | `collapsed: true` |\n| `defaultOpen: true` | `collapsed: false` |\n| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |\n| `pages: [...]` slugs | `pages: [...]` (ordering) |\n| `description` | **drop** (folders have no description) |\n| `collapsible: false` | that folder's `display: \"flat\"` (a plain heading, even under a global `\"group\"` mode) |\n\n`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`, `directory` (`\"card\"`/`\"accordion\"` lists the folder's pages below its `index` page — the replacement for a hand-written card list of the folder's children). Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: \"group\"` (drill-in is `\"page\"`); when the whole sidebar should collapse, set `navigation.sidebar.display: \"group\"` once in `blume.config.ts` instead of repeating it per folder.\n\n**Numeric filename prefixes move URLs.** Fumadocs builds slugs from the file path as written, so `01-intro.mdx` serves `/docs/01-intro`; Blume reads a `01-` prefix on a file or folder as sort order and drops it from the route (`/docs/intro`). Add a `redirects` entry for every prefixed file or folder (or pin the old path with frontmatter `slug`, a full path from the content root), and write `pages` entries without the prefix (`\"intro\"`, not `\"01-intro\"`).\n\n**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.\n\nHandle the `pages` array items:\n\n- **`\"...\"`** (rest marker) / `\"\"` → drop; Blume appends unlisted pages automatically. **`\"z...a\"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.\n- **A `pages` list with no `\"...\"`** → Fumadocs shows **only** the listed items; every page and folder it leaves out is hidden from the sidebar (still served by URL). Blume appends unlisted children, so each one **resurfaces** — set `sidebar.hidden: true` in each left-out page's frontmatter (for a left-out folder, in every page under it — a folder with no visible page drops out of the sidebar).\n- **`\"!page\"`** (exclusion from `\"...\"`) → the same trap: Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead.\n- **`\"---Section---\"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead.\n- **`\"...folder\"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.\n- **`\"[Text](url)\"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`).\n- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.\n\n## Frontmatter\n\nFumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). **`full: true`** (full width, TOC folded into a popover) → `mode: wide` (full column width, no TOC — the closest match; report); **`_openapi`** marks a generated API stub — delete the whole page (see OpenAPI). Any other non-schema key is a build error, so drop and report.\n\n## Icons: lucide-react names → kebab-case\n\nFumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`\"BookOpen\"`, `\"HomeIcon\"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons).\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare `<Callout>`** → `:::info`. `title` → `:::type[Title]`. An `icon` (a JSX element there) needs a `<Callout type=\"…\" title=\"…\" icon=\"cpu\">` with its kebab-case Lucide name, since a directive can't set one; drop it if it only restated the type's icon.\n- **Cards:** `<Cards>` → `<CardGroup>`. `<Card>` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={<Cpu />}` imported from `lucide-react`) → Blume takes a string name (`icon=\"cpu\"`); Fumadocs `description=\"…\"` → Blume has no `description` prop — move the text into the card body.\n- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).\n- **File trees:** `<Files>` → **`<Tree>`** (the JSX container for `Tree.*` children — _not_ `<FileTree>`, which wraps a Markdown list); `<Folder>` → `<Tree.Folder>` (`defaultOpen` carries over); `<File>` → `<Tree.File>`. Or convert the whole block to a list-driven `<FileTree>`.\n- **Tabs:** Fumadocs declares labels on the parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value=\"npm\">`. Blume's `<Tab>` carries its own `title`. Strip `items={[…]}` from `<Tabs>` and give each child `<Tab>` a `title` (from its `value`, or the positional `items` entry).\n- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.\n- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link: { href, text }, dismissible, id }`).\n- **`<include>./partial.mdx</include>`** — Blume uses the same include syntax. **Pass through unchanged**: paths resolve relative to the including file, a leading `/` resolves from the content root, and nested includes work. Keep partials in `_`-prefixed files or folders so they stay out of routing and the sidebar.\n- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.\n- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory the MDX components file before deleting it** — `components/mdx.tsx` (`getMDXComponents`) in newer scaffolds, `mdx-components.tsx` in older ones; components registered there are used import-free in MDX bodies; port or inline each usage first.\n\n## Headings\n\nTrailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from the TOC), `[toc]` (TOC-only entry) — use the same syntax in Blume. **Pass through unchanged.** One exception: Fumadocs' looser grammar accepts an id containing whitespace (`[#two words]`); Blume does not parse that as a marker, so rewrite such an id to a hyphenated one and update every link that targets it.\n\n## Code fences\n\n- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.\n- Adjacent fences sharing `tab=\"…\"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.\n\n## OpenAPI\n\n`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list (vendor the spec locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.\n\n## GraphQL\n\n`@fumadocs/graphql` works differently from the OpenAPI flow: **no stub files** — pages are virtual, served through the Loader API. The artifacts to harvest and tear down: `lib/graphql.ts` (`createGraphQL()` — read its `input` for the schema and any per-source routes/labels), the `graphql.staticSource()`/`graphql.loaderPlugin()` wiring in the source config, the `GraphQLPage` component (`createGraphQLPage()`, usually `components/api-page.tsx` — read its playground `endpoint`), and the `@fumadocs/graphql/css/preset.css` import. Map to a `graphql({ spec, endpoint })` entry in the top-level `reference` list (see SKILL.md \"GraphQL\"): SDL files/text and introspection results carry over as `spec` (vendor a URL input locally); a programmatic `GraphQLSchema` instance must be printed to SDL and committed (report); the `createGraphQLPage` playground endpoint becomes `endpoint`. When `createGraphQL()` takes several inputs, or sources carry their own routes, labels, or endpoints, map each one to an entry in the adapter's `sources` (`{ spec, route, label, endpoint }`) instead of the single `spec` shorthand — otherwise a schema is dropped or mounted on the wrong route. Blume groups routes the same way (`<route>/queries/<field>`, `<route>/objects/<type>`), but slugs are re-derived — rewrite inbound links and let `blume validate` catch strays. Then remove `@fumadocs/graphql`, the two lib/component files, and the CSS import. Since there are no generated pages, there is nothing to delete from the content tree — but keep any hand-written conceptual pages under the reference route.\n\n## i18n\n\nA `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's default `dir` parser as-is; locale **file suffixes** (`page.cn.mdx` beside an unsuffixed default-locale `page.mdx`) match the `dot` parser as-is — add `parser: \"dot\"`, no file moves. Under `dot` a folder's `meta.ts` serves every locale, so build it from the default locale's `meta.json` and report any translated folder titles in `meta.<lang>.json`. Report whichever transform you apply.\n\n## Package.json & teardown\n\nRepoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*`/`@fumadocs/*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `lib/source.ts`, `lib/shared.ts`, `lib/layout.shared.tsx` (or `app/layout.config.tsx`), `components/mdx.tsx` / `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).\n\n## Dropped — report these\n\nFolder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full` (approximated as `mode: wide`); header settings from `lib/layout.shared.tsx` with no Blume home; `<DynamicCodeBlock>`/`<InlineTOC>`; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.\n","isInternal":false,"tokens":4146,"sizeBytes":15685},{"name":"gitbook.md","path":".agents/skills/blume-migrate/references/gitbook.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/gitbook.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# GitBook → Blume\n\nGitBook is a hosted editor. This reference covers the Markdown it writes when a space syncs to GitHub or GitLab (Git Sync): `SUMMARY.md` (a space's navigation), `.gitbook.yaml` (where its files live, plus redirects), and, on site-wide Git Sync, `gitbook-docs.yaml` (which space maps to which directory). Theme, logo, header and footer links, custom domain, site redirects, and access control live in the GitBook app, not the repo. Two things set this migration apart:\n\n- **URLs follow `SUMMARY.md`, not the file tree.** Moving a page in the editor changes its URL but not its file. Blume derives route _and_ sidebar from the file path, so the core move is putting each file at its old URL.\n- **Blocks are Liquid-style tags (`{% hint %}`) and raw HTML inside `.md`.** Almost every page becomes `.mdx`, and MDX rejects several things GitBook writes.\n\n## Detect\n\n- **`.gitbook.yaml`** or **`.gitbook.yml`** (GitBook reads both) at the root or in a space directory; **`gitbook-docs.yaml`** for site-wide Git Sync.\n- A **`SUMMARY.md`** (GitBook writes it opening `# Table of contents`; a hand-written one may open `# Summary`), with `* [Title](path.md)` items and `## Group` headings, beside a **`.gitbook/`** folder (`assets/`, `includes/`, maybe `vars.yaml`). `.gitbook.yaml` is optional, so this pair can be the only sign.\n- Pages that open with a `# Title` line and use `{% hint %}`, `{% tabs %}`, `{% content-ref %}`; sync commits titled `GITBOOK-<n>: …`.\n- **Not this format:** `book.toml` + `src/SUMMARY.md` is mdBook. `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit — see [Legacy GitBook CLI](#legacy-gitbook-cli-bookjson).\n\n## First, collect what the repo doesn't hold\n\nDo this while the GitBook site is still published. Nothing moves before step 1.\n\n1. **Old URLs.** `<site>/sitemap.xml` indexes one `sitemap-pages.xml` per published space (`/sitemap-pages.xml`, `/de/sitemap-pages.xml`, …); fetch them all. They list every canonical page, hidden and generated OpenAPI pages included, but not `noIndex`/`noRobotsIndex` pages (take those from `SUMMARY.md`). `<site>/llms.txt` lists the same pages with their `.md` URLs. Save the paths, one per line, outside the content root.\n2. **Site redirects** (Settings → Redirects). The app has no export: `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/redirects?limit=1000` with a token (paginated: while the response has `next.page`, fetch again with `&page=<it>`), or ask the user. With neither, report it as a follow-up.\n3. **Site customization.** `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/customization` with a token. Without one, read the published HTML: logo and favicon URLs go through `~gitbook/image?url=<original>` (decode `url` and download it), the header carries a light logo (`class=\"block dark:hidden …\"`) and, when one is set, a dark one (`hidden dark:block`), and the inline CSS declares the primary color as an RGB triplet, `--primary-original`.\n4. **Spaces not in the repo.** Compare the sitemap index with the repo's directories. A space with `content.directory: null`, or a language or version the site serves but the repo lacks (often GitBook-made translations), has no Markdown here. Ask: export it (Git Sync that space into a branch, or `POST https://api.gitbook.com/v1/spaces/{spaceId}/git/export`) and migrate it; regenerate it with `blume translate` once `i18n` is set; or drop it and redirect its URLs (see URLs and redirects). Report the choice.\n\n## Config\n\nMap what step 3 found or the user supplies; leave the rest at Blume's defaults.\n\n| GitBook | Blume |\n| --- | --- |\n| Site title (`gitbook-docs.yaml` `site.title`) | `title` |\n| Logo (light and dark) | `logo: { image: { light, dark } }`, files in `public/` (SKILL.md covers monochrome SVGs and wordmarks) |\n| Icon (favicon) | `public/icon.svg` or `icon.png` (a dark one → `icon-dark.*`); no config field. An emoji icon has no file: make one or report it |\n| Primary color | `theme.accent` |\n| Default mode, fonts, code theme | `theme.mode`, `theme.fonts.{body,display,mono}`, `markdown.code.theme` (nearest Shiki themes) |\n| Theme style, tint and semantic colors, corner, depth, link, and sidebar styles | drop (report); a root `theme.css` if one matters |\n| Header links; a button-styled one | `navigation.actions`; `navigation.cta` |\n| Announcement | `banner` (`{ content, link, dismissible, id }`) |\n| Footer | `footer.links`, flattened into one row; copyright → `footer.copyright`; group titles and logo drop (report) |\n| Social accounts | `footer.socials` |\n| Social preview image | `seo.image` on the landing page (Blume renders a card for every other page) |\n| Git Sync repository and branch | `github: { owner, repo, branch }`, plus `dir` or `host` when needed; GitLab has no equivalent (report) |\n| Page actions: Markdown and AI providers, MCP, PDF | defaults; `agents.mcp.enabled` plus server output (follow-up); `export` |\n| GitBook Assistant | `ai.assistant` plus server output (follow-up), or drop |\n| Integrations | adapters in `analytics` from `blume/analytics`; others `script()` or report |\n| Custom domain | the DNS record moves to the new host; keep the URL as `deployment.site` unless the host is Vercel or Netlify |\n| Custom subdirectory (`example.com/docs`) | `deployment: { base: \"/docs\" }` |\n| Share links, authenticated access | host-level protection (the deployment docs' **Private docs**); report |\n| `.gitbook/vars.yaml` | `variables` (see Variables) |\n\n**Content root.** `root: ./docs/` is Blume's default `content.root`. With `root: ./`, `git mv` the space (pages, `SUMMARY.md`, `.gitbook/`) into `docs/` — routes are relative to the content root, so they don't change — or scope `content.include` (`references/monorepo.md` §1). Left unscoped, the repo's `README.md` and every stray `.md` publish.\n\n## Navigation: `SUMMARY.md` → folders\n\n### How GitBook builds a URL\n\n- The first `SUMMARY.md` item (`structure.readme`, `README.md` by default) is served at the space root; its own slug (`/readme`) redirects there.\n- Every other page: the space's prefix (see Site structure), then its `## Group`'s slug, each ancestor page's slug, and its own. A page's slug comes from its file name at import (a `README.md` → its folder's name), lowercased; a group's, from its heading (emoji dropped) or the `id` of an `<a id=\"…\">` inside it.\n- **The file's own path doesn't count.** A page filed at `configuration/post-install.md` but listed under group \"Deployment\" → parent `deployment/permissions/README.md` publishes at `/deployment/permissions/post-install`.\n\nCompute every page's URL from `SUMMARY.md`, then check them against the sitemap list; the sitemap wins.\n\n### Put every file at its old URL\n\nOne `git mv` per page keeps both the route and the sidebar:\n\n- A page with no children → `<url>.mdx`; with children → `<url>/index.mdx`; the landing page → `index.mdx` at the content root (`.md` for a page that stays plain). Lowercase every segment: Blume keeps a file name's case, so `OCR.md` would publish at `/OCR` where GitBook served `/ocr`.\n- **`README.md` isn't an index in Blume** (`guides/README.md` publishes at `/guides/README`): every README becomes `index`.\n- Each `## Group` becomes a top-level folder named for its slug.\n- Keep a table of old file → new file → route: links, includes, and image paths are rewritten from it.\n- A file not in `SUMMARY.md` was never published. Delete it, or prefix it with `_` to keep it unpublished, and report it.\n- Delete `SUMMARY.md` once the folders and `meta.ts` files carry its structure; it would publish as a page.\n\n### Order, labels, and display → `meta.ts`\n\n- **Order.** Each folder's `meta.ts` lists its children in `SUMMARY.md` order (`pages`); the content root's lists the loose pages, then the group folders.\n- **Labels.** A page's title is its `SUMMARY.md` link text, and a link title (`[Title](page.md \"Label\")`) → `sidebar.label`. A group folder's `meta.ts` `title` is the heading text, and **a parent page's folder gets `title: <its SUMMARY text>` too** — otherwise the row shows the humanized folder name (`deployment-guides` → \"Deployment Guides\" for a page titled \"Getting started\").\n- **Display.** GitBook shows groups as plain headings and each parent page as one expandable row. Set `navigation: { sidebar: { display: \"group\" } }`, `display: \"flat\"` in each group folder's `meta.ts`, and `sidebar: { hidden: true }` on each parent's `index`: the folder row already links to it, so without this the parent lists twice.\n- **Contents pages.** A parent whose body is empty or only content-refs to its own children → delete those and set `directory: \"card\"` in the folder's `meta.ts`. It inherits: set `directory: \"none\"` on a nested folder that isn't a contents page.\n- **Order Blume can't match.** Loose top-level pages always list above groups and folders. A parent page among loose pages (Overview, Editions ▸, Use Cases) drops below them, and pages after a `***` divider in `SUMMARY.md` (top-level again: `/uninstall`, not `/other/uninstall`) rise above the groups. Move such pages into a parenthesized group folder (`(more)/`, which adds no URL segment) ordered in the root `meta.ts`, or accept the order; report either.\n- **External links** (`* [Website](https://…)`) → `navigation.featured` or `navigation.actions`; report the move.\n- **Hidden pages.** GitBook's `hidden: true` hides a page's whole subtree; Blume's hides one page, so set it on the index and every descendant (a folder whose pages are all hidden leaves the sidebar).\n- A list item that is a YAML block with `type: builtin:openapi` is a generated API reference (see OpenAPI).\n\n### Site structure: `gitbook-docs.yaml`\n\n`site.structure` nests `section-group`, `section`, `space`, and `external-link` entries. Every entry has a `key` (meaningless to Blume), a `title`, and maybe `localizedTitle`; most can carry `draft` or `condition`. Sections and spaces have a `path` and maybe `default: true` (a section group has no `path`); section groups, sections, and external links can have an `icon`; a space can be `hidden` and has `content.directory` (`null` when not synced) and maybe `content.language`. Skip `draft` entries, and treat a `condition` like adaptive content (report it).\n\n- **URL prefixes.** The default section's default space is at the site root. Another space → `/<section>/<space>`; a non-default section's default space → `/<section>`. A site without sections serves its default space at the root and the rest at `/<space>`.\n- **Sections** → a top-level folder each plus a `navigation.tabs` entry (`icon` → Lucide). The default section stays at the content root; add `{ label, path: \"/\" }` to show it as a tab.\n- **Section groups** → a dropdown tab scopes the sidebar by one `path`, so give each section its own tab (the grouping is lost; report) or move them under one prefix (URLs change; pattern redirects).\n- **Spaces within a section:** languages → `i18n` (Blume puts the locale first, site-wide: `/payments/fr/x` → `/fr/payments/x`); versions → `versions` (top-level `/<id>/…`, ids start with a letter); products → a folder each and a `navigation.selectors` dropdown. Redirect the old prefixes.\n- A space's directory can sit inside another's (`./home/fr` in `./home`): separate them, then do the `SUMMARY.md` work per space. A `hidden: true` space is still published but left out of the space switcher: keep its pages, leave it out of `navigation.selectors`, and report it. `localizedTitle` → a per-locale tab label.\n- **External links** (`external-link`, at the root or in a section group) → `navigation.actions` entries (`{ label, href }`; `localizedTitle` → a per-locale `label`, the `icon` drops).\n\n## Content and components\n\n### `.md` or `.mdx`\n\nRename to `.mdx` every page with a `{% … %}` block, `$$` math, a mermaid fence, or a component after conversion. Directives, components, math, mermaid, `package-install`, and `ts2js` are MDX-only: in `.md`, a leftover `{% hint %}` or `:::note` is literal text with a green build (each `{% … %}` tag warns `BLUME_TEMPLATE_TAG` at its line, a `:::note` `BLUME_MD_DIRECTIVE`), and in `.mdx` a leftover `{% … %}` fails it (`BLUME_MDX_SYNTAX`, which `blume validate` and `blume check` report too). Plain pages can stay `.md`; tables, footnotes, fence meta, `<include>`, `{{variables}}`, `[#id]`, and raw HTML work in both. An include is spliced as text, so it's parsed in the including page's format.\n\n### MDX hazards\n\nEach of these fails `blume build` in `.mdx` (`blume check` and `validate` name the ones MDX can't parse as `BLUME_MDX_SYNTAX`, at their line):\n\n- **Unclosed `<img …>` and `<br>`** (`BLUME_MDX_UNCLOSED_ELEMENT` names each). Close them — better, convert images to Markdown.\n- **HTML comments** → `{/* */}`.\n- **A `{` outside code.** GitBook's `\\{{x\\}}` isn't enough; write `` `{{x}}` `` or `\\{\\{x\\}\\}`. Braces inside raw HTML (`<code>CN={{DeviceId}}</code>`, `<td>${EMAIL}</td>`) compile, then fail the render: `blume build` names each as `BLUME_MDX_UNDEFINED_NAME` at its line. Turn `<code>`, `<strong>`, `<a>`, `<p>`, and `<br>` inside table cells into Markdown.\n- **Markdown inside JSX** (`<Tab>`, `<Step>`, `<Frame>`) needs blank lines around it.\n\nGitBook's `\\<` and entities (`&#x20;`, `&#x3C;`) are fine (an escaped `\\<Name` can still warn `BLUME_UNKNOWN_COMPONENT`; the page renders correctly). Strip the U+200B/U+200C characters GitBook leaves at the start of some code lines.\n\n### Blocks\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` (the first line) | frontmatter `title`; delete the line |\n| `{% hint style=\"info\" %}` (`success`, `warning`, `danger`) | `:::info` (`:::success`, `:::warning`, `:::danger`); legacy plugin `tip` → `:::tip`, `working` → `:::note`. A directive takes no `icon`: keep one that matters as `<Callout type=\"<type>\" icon=\"<lucide>\">`, else drop it; inside another directive, give the outer one a longer fence (`::::`) |\n| `{% tabs %}` + `{% tab title=\"X\" icon=\"y\" %}` | `<Tabs>` + `<Tab title=\"X\" icon=\"<lucide>\">`; `fullWidth` drops. Blume switches same-titled tabs together page-wide (`sync={false}` opts a group out) |\n| `{% code title=\"a.js\" lineNumbers=\"true\" overflow=\"wrap\" expandable=\"true\" %}` | fence meta: ` ```js title=\"a.js\" lineNumbers wrap expandable `; `fullWidth` drops. **A fence with no language takes `text`** (` ```text lineNumbers `), or the first option becomes the language (`BLUME_UNKNOWN_CODE_LANGUAGE`) |\n| `<pre class=\"language-x\" data-title=\"…\" data-line-numbers data-overflow=\"wrap\"><code>` | a fence built from those attributes (others drop), entities decoded, `<strong>`/`<em>` stripped |\n| `{% stepper %}` + `{% step %}` | `<Steps>` + `<Step>`. **A step that opens with a heading keeps it as the first line inside `<Step>`**: GitBook publishes it as an anchored heading in the outline, and `title` renders a plain paragraph with no anchor. A bold-line opener → `title` |\n| `{% columns %}` + `{% column %}` | `<Columns cols={n}>` + `<Column>`; widths drop |\n| `<details><summary>T</summary>` (`open`) | `<Expandable title=\"T\">` (`defaultOpen`); a run → `<Accordion>` + `<AccordionItem title=\"T\">`. A title renders inline Markdown, so write a summary's `<code>`, `<strong>`, and `<a>` as Markdown in it: raw HTML there shows as text |\n| `{% content-ref url=\"x.md\" %}`; legacy `{% page-ref page=\"x.md\" %}` | `<Card title=\"<x's title>\" href=\"<x's route>\" />` (the link text is a file name, so look the title up); a run → `<CardGroup>`; links to a parent's own children → `directory: \"card\"` |\n| `[text](x.md#h \"mention\")` | a plain link without `\"mention\"`, its text replaced by the target's title |\n| `{% embed url=\"…\" %}`, often with no `{% endembed %}` | YouTube → `<YouTube url=\"…\" />` (`?t=122` → `start={122}`; `url` ignores `t`); anything else → `<Card title=\"…\" href=\"…\" />`. A caption → `<Frame caption=\"…\">` around it (YouTube's `title` only reaches screen readers) |\n| `{% file src=\"…\" %}caption{% endfile %}` | `[caption](../assets/x.pdf)`, relative to the page: Blume publishes a file a relative link names with the page. For a URL that never changes, put the file in `public/files/` and link `/files/x.pdf` |\n| `<figure><img src alt><figcaption>` | `![alt](relative/path.png)` (optimized); a caption → `<Frame caption=\"…\">` around it; `<div data-with-frame=\"true\">` → `<Frame>`. A raw `<img>` with a relative `src` works too, but unoptimized |\n| `<img class=\"gitbook-drawing\">` | a Markdown image |\n| inline `<img data-size=\"line\">` | a Markdown image (renders at its own size), or `<img src=\"/<public path>\" alt=\"\" height=\"20\" />` to stay text-height (report) |\n| `<picture>` with a dark-mode source | the light image (report) |\n| `<table data-view=\"cards\">` | `<CardGroup cols={3}>` (`data-card-size=\"large\"` → `cols={2}`), a `<Card>` per row: first text cell → `title`, other text → body, the `data-card-target` link → `href`, the `data-card-cover` file → `img`, relative to the page like a Markdown image (`img=\"../assets/cover.png\"`), an `<i class=\"fa-…\">` cell → `icon` |\n| other `<table>` (`data-header-hidden`, `data-search`, cells with `<p>`/`<br>`) | a GFM table (a hidden header's first row becomes the header); a cell's `<ul><li>` can stay inline in `.mdx` |\n| `$$…$$`, a ` ```mermaid ` fence | unchanged, in `.mdx` |\n| Emoji: `:rocket:`, escaped `:ballot\\_box\\_with\\_check:`, `<span data-gb-custom-inline data-tag=\"emoji\">☑️</span>` | the character itself: Blume renders no shortcodes |\n| `<i class=\"fa-x\">:x:</i>` | `<Icon icon=\"<lucide>\" />`, or delete it |\n| `<a class=\"button primary\">`, `<button data-action=\"…\">` | a link or a `<Card>` with `cta`; search and ask buttons drop |\n| `<mark style=\"color:…\">` | `<mark>` or bold; report colors that carried meaning |\n| `## Heading <a href=\"#x\" id=\"x\"></a>` | works as written: the heading takes `x` as its id. `## Heading [#x]` is the tidier spelling |\n| `[^1]` annotations | unchanged: footnotes at the page's end, not hover popups |\n| `{% updates %}` + `{% update date=\"…\" %}` | `type: changelog` pages (SKILL.md \"Changelogs\"), or dated `##` sections |\n| `{% prompt description=\"…\" %}` | `<Prompt description=\"…\">` |\n| `{% if visitor… %}` | keep what the public saw, drop the tags (report) |\n\n### Links\n\n- Every moved file breaks the relative links into and out of it. Rewrite internal links from your table as root-absolute routes (`/deployment/permissions#roles`): Markdown links, content-ref URLs, card `href`s, and `href`s in HTML. A `README.md` target → its folder's route; absolute links to the old domain → routes.\n- Cross-space links `https://app.gitbook.com/s/<spaceId>/<path>` (or `…/o/<orgId>/s/…`) → the target space's prefix plus `<path>`; `GET https://api.gitbook.com/v1/spaces/{spaceId}` gives its `urls.published`. Links into `…/~/changes/…` or to `/broken/pages/<id>` were already broken: fix or report them.\n\n### Images and assets\n\n- Move `.gitbook/assets/` to `assets/` at the space root and replace the string `.gitbook/assets/` with `assets/`, then recompute the paths in every moved page.\n- File names have spaces and parentheses. `![](<../assets/image (1).png>)` and `../assets/image%20(1).png` both work, in a page and in an `_includes/` partial alike: a partial's images are rebased onto the including page in any form.\n- GitBook keeps every upload, so many files in `assets/` may be referenced by nothing. Report them and offer to prune.\n\n### Variables and expressions\n\n- `.gitbook/vars.yaml` → `variables`; `<code class=\"expression\">space.vars.NAME</code>` → `{{NAME}}`.\n- `page.vars.NAME` (from `vars:` frontmatter) has no page scope: write the value inline. Read inside an include, it becomes a prop: `<include name=\"Computer\">/_includes/x.md</include>` with `{{name}}` in the partial (prop names are lowercase).\n- An expression with logic (`a ? \"x\" : \"y\"`) → evaluate it and write the result.\n- Once `variables` is set, an undefined `{{name}}` in prose fails the build: placeholders that are content (`{{DeviceId}}`) go in inline code.\n\n### Includes (reusable content)\n\n- `.gitbook/includes/x.md` → `_includes/x.md` at the content root, and `{% include \"../../.gitbook/includes/x.md\" %}` → `<include>/_includes/x.md</include>` on its own line (a leading `/` resolves from the content root).\n- **Demote every heading in an include one level:** GitBook renders an include's `#` at a page's `##`.\n- Delete includes nothing references. One referenced by ID (`{% include \"/reusable-content/…\" %}`) isn't in the repo: copy it from the app.\n\n### OpenAPI\n\nGitBook has five forms: a `SUMMARY.md` YAML item `type: builtin:openapi` naming an organization spec (`dependencies.spec.ref.spec: <slug>`); `{% openapi src=\"…\" path method %}`; `{% openapi-operation spec=\"<slug>\" path method %}` and `{% openapi-schemas %}`; the older `{% swagger src=\"…\" path method %}`; and hand-written `{% swagger method path baseUrl %}` blocks with `swagger-parameter` and `swagger-response` (oldest: `{% api-method %}`).\n\n- **Specs.** Vendor each one into the repo — a `<slug>`'s file comes from the app's OpenAPI panel or `GET https://api.gitbook.com/v1/orgs/{organizationId}/openapi/{specSlug}/versions/latest/content/raw` — and add `openapi({ spec })` from `blume/reference` with a `navigation.tabs` entry (SKILL.md \"OpenAPI\").\n- **`builtin:openapi`** → set `route` to the old reference's path and keep its parent page's prose under it. Operation URLs change, so redirect each old one in the sitemap (SKILL.md \"OpenAPI\" covers generating them).\n- **Inline operations** → a link or `<Card>` to the generated page. `{% openapi-schemas %}` has no counterpart (report).\n- **Hand-written blocks** → a `<ParamField query|path|header|body=\"…\">` per parameter (a `cookie` has no slot), the responses as titled fences in `<ResponseExample>`, and `api: \"GET /path\"` frontmatter when it's the page's only endpoint (`baseUrl` → `api.server`).\n- `x-codeSamples` carries over; GitBook's other `x-*` extensions are ignored (report).\n\n## Frontmatter\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` / `SUMMARY.md` link text | `title`; where they differ, GitBook published the `SUMMARY.md` text |\n| `SUMMARY.md` link title | `sidebar.label` |\n| `description` (often folded `>-`) | `description` (quote a plain value containing `: `) |\n| `icon` (Font Awesome) | `icon`, in Lucide (see Icons) |\n| `hidden: true` | `hidden: true`, on the page and every descendant |\n| `noIndex: true` | `search.exclude: true` + `noindex: true`, on the page and every descendant |\n| `noRobotsIndex: true` | `noindex: true`, on the page and every descendant |\n| `cover`, `coverY`, `coverHeight`, `tags`, `if` | drop (report); a cover worth keeping → an image at the top of the body |\n| `vars` | inline the values, then drop |\n| keys left by older tooling (`category`, `order`) | drop: GitBook ignored them |\n\n`layout`:\n\n| GitBook `layout` | Blume |\n| --- | --- |\n| every part visible, `width: default` | delete the block |\n| `tableOfContents.visible: false` | `mode: center`; with `title.visible: false` too, `mode: custom` |\n| `outline.visible: false`, or `width: wide` | `mode: wide` |\n| `title.visible: false`, sidebar shown | `mode: frame` |\n| `pagination.visible: false` | `pagination: false` |\n| other parts hidden; a preset (`layout: landing`) | drop (report); for a preset, match the published page |\n\nA Blume mode hides more than one part: `center` and `wide` also hide the outline, and `frame` hides it and the page-end links and footer. Report it where GitBook kept those.\n\nBlume's frontmatter is strict: remove every key the tables drop.\n\n## Icons\n\nGitBook uses Font Awesome names in frontmatter `icon`, `<i class=\"fa-…\">`, tab, hint, and button icons, and section icons. Blume is Lucide-only.\n\n- **Frontmatter:** `node <skill>/scripts/mintlify-codemod.mjs <content-dir>`, then `--write`. Its icon pass is the same Font Awesome → Lucide map, and GitBook frontmatter has none of the keys its other rules touch.\n- **Everything else by hand,** with the table in `references/mintlify.md`. Common GitBook names: `circle-info` → `info`, `clock-rotate-left` → `history`, `sliders` → `sliders-horizontal`, `handshake-angle` → `handshake`, `angles-up` → `chevrons-up`, `puzzle-piece` → `puzzle`, `life-ring` → `life-buoy`, `shield-halved` → `shield`, `file-contract` → `file-text`.\n- **Check every name** against the set Blume bundles; an unknown name renders nothing:\n\n  ```bash\n  node -e 'const b=require.resolve(\"blume/package.json\");const s=require(require.resolve(\"@iconify-json/lucide/icons.json\",{paths:[b]}));for(const n of process.argv.slice(1))console.log(n,!!(s.icons[n]||s.aliases?.[n]))' zap history sliders-horizontal\n  ```\n\n## URLs and redirects\n\nWith every file at its old URL, pages need no redirects. The rest are below. GitBook answers all of them with a 307, except a permanent site redirect (308). Blume's default `status` is 301: keep it for a settled move (`.gitbook.yaml` redirects, recovered automatic redirects), and set `status: 307` where the target may change (group paths, dropped translations).\n\n- **`.gitbook.yaml` `redirects`** (`old/path: new/file.md`, relative to the space) → `{ from: \"/<space prefix>/old/path\", to: \"<new/file.md's route>\" }`.\n- **Site redirects** (`source`, `destination`, `permanent`, `captureWildcard`, `draft`) → `{ from: source, to, status: permanent ? 308 : 307 }`. `destination` is an object, not a URL: `kind: \"external\"` carries `url`; `site-page` (`siteSpaceId`, `pageId`), `site-space`, and `site-section` name ids. Resolve those to the old URL — `GET …/sites/{siteId}/site-spaces` gives each site space's `urls.published` and `space.id`, `…/sites/{siteId}/sections` each section's `urls.published`, and `GET /v1/spaces/{spaceId}/content` each page's `path` within its space — then to the Blume route at that URL. A trailing `*` with `captureWildcard` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`; without it, every match goes to the one `to`. Skip drafts (the list leaves them out by default), and write `from` without the site base.\n- **Automatic redirects.** GitBook quietly 307s every former URL of a moved or renamed page, and lists them nowhere. Recover them: run every historical `SUMMARY.md` (`git log --format=%h -- SUMMARY.md`, then `git show <sha>:SUMMARY.md`) through the URL rule above, add the route each file's path implies, and request each candidate from the live site (`curl -s -o /dev/null -w '%{http_code} %{redirect_url}'`). Each 307 to a page you kept becomes a redirect.\n- **Group paths and the landing slug.** GitBook redirects a group's own path to its first page, and `/readme` to `/`. Add both (`status: 307` for the group paths).\n- **Dropped translations.** GitBook translates group slugs (`/de/zahlungen/…` for group \"Payments\"), so `/de/:path*` → `/:path*` sends most translated URLs to 404s. Diff each language's `sitemap-pages.xml` against the default one, add a pattern per translated group (`{ from: \"/de/zahlungen/:path*\", to: \"/payments/:path*\", status: 307 }`), then each language's catch-all last: patterns are tried in order.\n- **Case.** GitBook matched URLs case-insensitively; Blume doesn't. Lowercased files cover the canonical URLs; report mixed-case inbound links.\n- **Markdown URLs.** GitBook serves `<url>.md`, and so does Blume; redirects move the `.md` URL with its page.\n- Rewrite internal links **before** adding redirects: `blume validate` accepts a redirect's `from` as a link target.\n\n### Heading anchors\n\nGitBook makes heading ids its own way: colons, slashes, and a spaced dash become one hyphen each, dots stay, an id that would start with a digit gets `id-`, and ids stop at 100 characters (`AppConfig:OCSP:UseAuthorizedResponder` → `appconfig-ocsp-useauthorizedresponder`, where Blume writes `appconfigocspuseauthorizedresponder`). Every `#anchor` link GitBook wrote to such a heading fails `blume validate`, and links from other sites land at the top of the page. A rule predicted only 890 of 934 ids on a real site, so read them instead. After `blume build`, from the project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com          # report\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com --write  # pin\n```\n\nIt fetches each old page once (one request at a time, cached in `.pin-heading-ids-cache/`; delete it after), pairs its headings with the build's by text, and appends the old id to each differing source heading — `[#id]` in `.mdx`, `{#id}` in `.md`, `\\{#id\\}` in a partial. Rebuild and rerun until it reports 0. A `NOTE … no source line` is a heading the source doesn't spell out (a component renders it, or it was reworded): pin it by hand. `--only-linked` pins only ids that content links to. A page whose URL changed (a space under a new prefix) is looked up on the old site through `--map`, a JSON list of exact `{ from, to }` entries (patterns are skipped): `dist/blume-redirects.json` works as it is, but a build writes it only when no host adapter is set. Any `BLUME_BROKEN_ANCHOR` left after that was already dead on GitBook: retarget the obvious ones, drop the fragment from the rest, and report them.\n\n### Final check\n\nRequest each old URL from `blume dev` or `blume preview` (both apply pattern redirects) with `curl -sL`, or check it against `dist/` and `dist/blume-redirects.json` (written only when no host adapter is set), then run `blume audit --only redirects` after the build.\n\n## Legacy GitBook CLI (`book.json`)\n\nThe old `gitbook-cli` toolchain and its fork HonKit:\n\n- **`book.json`:** `root`, `structure`, `title`, `description`, `variables`, `plugins`. Map `title` and `description`; `root` → `content.root`; plugins → whatever Blume has built in, else report.\n- **URLs are file paths:** `a/b.md` → `/a/b.html`, a `README.md` → its folder's `index.html`. Keep the files where they are, rename READMEs to `index`, and add an exact redirect from each `.html` URL (a pattern can't strip the extension).\n- **`SUMMARY.md`** nests the same way, but `### Part` headings and `---` lines add no URL segment → `(part)/` group folders. Entries pointing at anchors (`page.md#x`) aren't pages.\n- **Still applies:** `{% hint %}` → directives; `{% include \"./x.md\" %}` → `<include>`; `{{ book.name }}` → `variables` and `{{name}}`; other Nunjucks logic → evaluate and inline; `LANGS.md` → `i18n`, with the default language at the content root; `GLOSSARY.md` → no equivalent (report).\n\n## Teardown\n\n- **Turn off Git Sync** for every mapped space before the converted content reaches the synced branch: GitBook imports each push to it and commits its own edits back. Keep the GitBook site published until the domain moves.\n- Delete `SUMMARY.md`, `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, and the emptied `.gitbook/`.\n- Check CI and scripts that read `.md` files or `.gitbook/assets/`: a Markdown link checker breaks on root-absolute routes and skips `.mdx` (replace it with `blume validate --strict`), and an image de-duplication script that greps `*.md` would now delete images in use. Remove GitBook agent instructions (an `AGENTS.md` block, a vendored GitBook skill).\n- A synced repo usually has no `package.json`: scaffold one (SKILL.md step 6).\n\n## Dropped — report these\n\nSite themes and styles beyond accent, fonts, and mode; footer group titles and logo; page covers; page and update tags; adaptive content and visitor authentication; GitBook Assistant unless the user adds `ai.assistant`; hint icons you didn't keep; button styles and search/ask buttons; `<mark>` colors; `fullWidth` and column widths; non-YouTube embeds as players; dark card covers and theme-aware images; inline image sizing; `{% openapi-schemas %}` and GitBook `x-*` spec extensions; automatic redirects you couldn't recover; case-insensitive URLs; page-group icons; sidebar positions Blume can't match (parent pages among loose pages, `***` dividers, external links); section groups flattened into tabs; spaces and translations not in the repo; integrations with no adapter; site redirects nobody could export; anything set only in the app that the user didn't supply.\n","isInternal":false,"tokens":8616,"sizeBytes":32305},{"name":"github-wiki.md","path":".agents/skills/blume-migrate/references/github-wiki.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/github-wiki.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# GitHub Wiki → Blume\n\nA GitHub wiki is a git repository beside the main one (`https://github.com/<owner>/<repo>.wiki.git`) that GitHub renders at `https://github.com/<owner>/<repo>/wiki`. It has no config file and no build: every file in a markup format GitHub renders is a page, `_Sidebar.md` and `_Footer.md` add the chrome, and pages link each other with `[[Page Name]]`. Five things set this migration apart:\n\n- **Folders don't count.** A page's URL is its file name, wherever the file sits (`codebase/compiler/Codebase-Compiler-Binder.md` is `/wiki/Codebase-Compiler-Binder`), and relative links and images resolve from `/<owner>/<repo>/wiki/`, never from the file's folder.\n- **`[[Link text|Page Name]]` puts the text first** in Markdown pages. GitHub's docs show the reverse, which is right only in `.mediawiki` pages, and Obsidian's `[[Page|text]]` is the reverse too, so don't point the `obsidian()` source at a wiki.\n- **Blume renders `[[…]]` as literal text.** It warns `BLUME_WIKILINK_UNSUPPORTED` only for a wiki link that names one of the site's pages or has a `|`, so `[[Page that doesn't exist]]` ships with a green build. Every wiki link has to be converted.\n- **The old URLs live on github.com,** which you can't redirect. What you can do there is in URLs and the old wiki below. Never tell the user the old links will redirect, and add no `redirects` for them.\n- **Nothing pins the routes,** so the new site gets clean ones: one flat lowercase route per page, as the wiki had one flat namespace, with the sidebar groups as `(group)/` folders.\n\nScope: Markdown pages fully. Pages in the other formats GitHub renders get converted to Markdown first (Other markup formats). Checked against GitHub's wiki docs, the live rendering of public wikis (microsoft/TypeScript, microsoft/vscode, koreader, ruTorrent, qBittorrent, mpv, gollum, fzf, AdGuardHome) in October 2026, and an end-to-end migration of the microsoft/TypeScript wiki (77 pages). GitHub Enterprise Server wikis use the same files but weren't checked.\n\n## Detect\n\n`blume migrate` can't detect a wiki: name it, `npx blume migrate github-wiki`. A wiki clone has:\n\n- a remote ending in `.wiki.git` (`git remote -v`);\n- `Home.md` (or `Home` in another format) at the root, often `_Sidebar.md` and `_Footer.md`;\n- page files named like URLs (`Getting-Started.md`, `Standalone-Server-(tsserver).md`, sometimes `Roadmap‐2021‐2022.md` with U+2010 hyphens), `[[Page Name]]` links, no config, and usually no `package.json`.\n\n**Not this format:** Docsify keeps a lowercase `_sidebar.md` beside an `index.html` that sets `window.$docsify` (`references/docsify.md`). On macOS and Windows the file system ignores case, so `_Sidebar.md` and `_sidebar.md` are the same name to `ls`, `test -e`, and Node's `existsSync`: check the real case with `git ls-files`.\n\n**Get the pages from the default branch.** `git clone https://github.com/<owner>/<repo>.wiki.git`, then `git remote show origin` names the HEAD branch. GitHub publishes only that branch, so other branches never showed. If the wiki is mirrored from a normal repository (a sync workflow, or a footer saying \"fork it and send a pull request\"), the wiki clone is still what readers saw.\n\n## Where the Blume project goes\n\nThe wiki repository can't host the new site: GitHub runs no Actions in it, serves no Pages from it, and accepts no pull requests. Ask the user where the docs will live:\n\n- **A folder in the main repository** (the usual choice: the docs then change in the same pull requests as the code). Follow `references/monorepo.md` for a code repo's root `package.json`, `dist/`, and workspace.\n- **A new repository**, for docs with their own maintainers.\n\nWithout an answer, convert a copy of the wiki clone in place: `blume.config.ts` and `package.json` at its root, the pages moved into `docs/`, so the user can move the folder as one unit. **Before you start, run `git remote remove origin` in the copy.** Pushing the migrated tree to the wiki would publish `blume.config.ts`, every moved page, and the stubs you prepare later as wiki pages.\n\nWiki history doesn't come along with a plain copy. To keep it for `git log --follow` and blame, bring the wiki in with `git subtree add --prefix=<folder> https://github.com/<owner>/<repo>.wiki.git <branch>` and say so in the report. Every page gets moved and edited either way, so `lastModified: \"git\"` would date every page to the migration commit until it's edited again (Blume follows a rename only when the file didn't change).\n\n## Run the codemod\n\nFrom the copy of the wiki clone (when the docs go into another folder, copy the wiki's files there first and run it from there), before any hand edits:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo>            # report\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write    # write the route table\n# classify the pages in wiki-routes.json (next section)\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo>           # dry run\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo> --write   # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent (a second `convert` skips the pages it already moved). Add `--mirror <owner>/<repo>-wiki` when a repository mirrors the wiki, and `--emoji emojis.json` (from `curl -s https://api.github.com/emojis`) when pages use `:shortcodes:`.\n\n- **`routes`** lists the pages the way GitHub serves them and writes `wiki-routes.json` (page name → new route) and `wiki-files.json` (page name → file) beside `blume.config.ts`. It prints the files GitHub never served, routes that clash, digit-led routes, non-Markdown pages, and its guesses at moved notices and non-content pages. It never overwrites an existing table: it lists what differs instead.\n- **`convert`** does everything in the Navigation, Content, and Images sections below that the table and `_Sidebar.md` decide: moves each kept Markdown page to `docs/(group)/<route>.md` (`.mdx` when it needs MDX) with its frontmatter, converts wiki links, wiki URLs, alerts, fences, headings, named anchors, and duplicate link definitions, moves the wiki's images beside the pages, and writes every `meta.ts`. It prints the `navigation.featured` entries for `blume.config.ts`, the footer's text, and a `REVIEW` line for everything that needs judgment. **Every `REVIEW` line is a to-do**; the sections below say what to do with each.\n- **`check`** (after `blume build`) and **`stubs`** (for the old wiki) are in URLs and the old wiki.\n\nThe sections below are the rules the codemod applies, for checking its output and for anything you do by hand.\n\n## The page list and the route table\n\n**Which files are pages.** Every file whose extension GitHub renders: `.md`, `.markdown`, `.mdown`, `.mkdn`, `.mediawiki`, `.wiki`, `.textile`, `.rst`, `.rest`, `.org`, `.creole`, `.asciidoc`, `.adoc`, `.asc`, `.pod`, `.rdoc`. The page name is the file name without its last extension (`README.html.md.md` is the page `README.html.md`), with spaces read as `-`. Matching is case-insensitive, `-` and space are the same, and U+2010 is not `-`.\n\n- **Not pages:** `_Sidebar.*` and `_Footer.*`, in any folder (GitHub even took `_Sidebar.html.md` as a sidebar). Every other `_`-prefixed file is a page (`_ [obsolete] Config.rest`), and Blume skips `_` files by default, so the rename matters.\n- **Two files with the same name** in different folders share one URL, and GitHub serves one of them (the root one, on the wiki checked). The other was never reachable: leave it out and report it.\n- **Every other file** (images, PDFs, `LICENSE`) is served raw at `/wiki/<path>`, which redirects to `https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`.\n- **GitHub's own list** is `https://github.com/<owner>/<repo>/wiki/_pages` (every page, each linked at its URL). Compare the table with it.\n\n**Old URLs.** `Home` is `https://github.com/<owner>/<repo>/wiki` (`/wiki/Home` 301s there); every other page is `https://github.com/<owner>/<repo>/wiki/<Page-Name>`, percent-encoded.\n\n**New routes.** Each page's route is its name, lowercased, with apostrophes dropped and every other run of characters that aren't letters or digits turned into one `-` (`Standalone-Server-(tsserver)` → `/standalone-server-tsserver`, `What's-new-in-TypeScript` → `/whats-new-in-typescript`, `tsconfig.json` → `/tsconfig-json`); non-Latin letters stay. `Home` becomes `/`. Give one of two clashing pages another route by hand. A route that starts with digits and a separator (`/5-0-release-notes`) would lose them as an ordering prefix, so `convert` pins it with `slug`.\n\n**Classify every page** by editing its entry in `wiki-routes.json`:\n\n- **Content:** keep the route.\n- **A moved notice** (the whole page says it moved: \"This page has moved to <URL>\"): set its entry to that URL, or to the target page's route (with its `#anchor`) when the URL is another page of this wiki. It becomes no page; links to it go straight to the target. A short page that points elsewhere without saying it moved (\"An introduction can be found in the handbook\") is content: keep it.\n- **Not content:** a mirror repository's `README.md`, `SECURITY.md`, or `CONTRIBUTING.md`, test pages, script fixtures, and pages that only say \"deprecated\" with nowhere to point. Set the entry to `null` and list it in the report (SKILL.md step 2).\n\n`routes` prints its guesses for the moved notices and the non-content pages; confirm each one.\n\n## Config\n\nWikis have no config: map what the repository and the special pages say.\n\n| GitHub wiki | Blume |\n| --- | --- |\n| Repository name | `title` (the project's name, or \"`<Project>` wiki\") |\n| Repository description (About) | `description` |\n| The repository | `navigation.repo: \"https://github.com/<owner>/<repo>\"` for the footer link. Set `github: { owner, repo, branch, dir }` only once the pages live in a repository, so **Edit on GitHub** opens the new file. Leave it unset while the home is unknown, and report it |\n| An image at the top of `_Sidebar.md` | `logo`, with the file in `public/` |\n| External links in `_Sidebar.md` | `navigation.featured`: `convert` prints the array |\n| `_Footer.md` links | `footer.links` |\n| `_Footer.md` text | a copyright or license line → `footer.copyright`, as plain text (Markdown stripped); other text → drop (report), or a `PageFooter` layout slot (`defineComponents`). A \"contribute to this wiki\" footer drops with its link: edit links replace it |\n| \"edited this page … · N revisions\" under each title | drop (report). `lastModified: \"git\"` would show the migration date on every converted page |\n| Wiki search, the Pages list, the per-page outline, the clone URL box | built in, or drop |\n| Site URL | none existed. Ask where the site deploys and set `deployment` (SKILL.md). GitHub Pages: https://useblume.dev/guides/markdown-docs-github-pages, with `deployment: { site: \"https://<owner>.github.io\", base: \"/<repo>\" }` for a project site |\n\n## Navigation: `_Sidebar.md` → `(group)/` folders\n\nGitHub showed two things in the right rail: the **Pages** list (every page, alphabetical) and, under it, `_Sidebar.md` rendered as Markdown. The sidebar becomes folders, and every page from the Pages list stays reachable.\n\n- **Each sidebar group becomes a `(group)/` folder** (SKILL.md): it adds the sidebar group and no URL segment, so every route stays the flat one from the table. Its `meta.ts` has `title` (the label as written) and `pages` (every child in sidebar order). The content root's `meta.ts` lists the groups in sidebar order.\n- **Group labels** are whatever heads a list: a bold line (`**News**`), a heading (`## Contributing`), or an unlinked list item with children (`* Testing`, a nested `(group)/` folder with `display: \"group\"`). A heading that links a page (`# [Tips and Tricks](…/wiki/Tips-and-Tricks)`) makes that page the group's own: a real folder with the page as its `index.md`. A heading that links outside (`## [User documentation](https://…)`) is a `navigation.featured` link.\n- **A page with nested pages under it** becomes a real folder named for its route, holding the page as `index.md`, its children, and a `meta.ts` with `pages` and `display: \"group\"`. Each child gets `slug: <its route without the slash>` so its route stays flat. Without `display: \"group\"`, a nested folder lists after every loose page of its parent group, not where the sidebar had it. Blume shows the parent both as the group header and as its first row; that's expected, so don't hide it.\n- **External links** can't sit inside a generated group, so they move to `navigation.featured` (above the sidebar, on every page); report the move. A link to a heading (`[Installation](…/User-patches#installation)`) has no row in Blume: drop it (the page's outline lists it) and report it.\n- **A page listed twice** stays in its first group; report the other entry. `[[Home]]` in the sidebar drops: the home page is `docs/index.md`, listed first, outside every group.\n- **Pages the sidebar doesn't list** were still in the Pages list: they go in a last group, `(more)/` with `title: \"More pages\"`, `display: \"group\"`, `collapsed: true`, and a `pages` list sorted by title (Blume's own order goes by file name, which differs for names like `'this' in TypeScript`). Pages that shared a wiki folder can get their own collapsed group instead (`(codebase)/`); report the groups you made.\n- **No `_Sidebar.md`:** GitHub showed only the Pages list, so the pages sit at the content root (Blume sorts them by title), or in groups by the wiki's folders if it had any; report the choice.\n- **A `_Sidebar.md` inside a folder** replaced the root one for that folder's pages. Usually it's a translation (`ru/_Sidebar.md` beside `ru/Home.ru.md`): map the folder to `i18n` when the pages pair up, else to a `navigation.tabs` entry, and report it.\n- Prose, images, and HTML lists in the sidebar have no place in Blume's sidebar: move a logo to `logo`, put useful prose on the home page, and report the rest.\n\n## Content\n\n### Page files and titles\n\n- **Each kept page** lands at `docs/(group)/<route>.md`, `Home` at `docs/index.md`. It stays `.md` unless it needs MDX (GitHub alerts, mermaid, math), then it's `.mdx` (MDX hazards below).\n- **`title`** is the page name as GitHub showed it above the page: `-` → space, U+2010 → `-` (`Standalone-Server-(tsserver)` → `Standalone Server (tsserver)`, `'this'-in-TypeScript` → `'this' in TypeScript`), quoted in YAML where needed.\n- **Body H1s.** GitHub drew the name as the title and every `#` in the body below it, so a page's H1s are sections, not its title (`# TypeScript 5.3` on a page named \"API Breaking Changes\"). A leading H1 that repeats the page name (ignoring case and punctuation) is deleted. If the page still has an H1, every heading is demoted one level (`#` → `##`, `##` → `###`, …; `######` stays), and a one-line setext heading becomes ATX one level down (`===` → `##`, `---` → `###`). A setext heading over several lines has no ATX form: `convert` reports it, so rewrite it as one `###` line by hand. Heading ids come from text, so demoting changes none.\n- **`sidebar.label`** is the sidebar's text for the page, when it differs from `title` by more than case and `-` versus space.\n\n### Wiki links\n\nEvery `[[…]]` outside code converts. GitHub leaves `[[ … ]]` in fenced code alone (bash tests), and so does `convert`: it skips fenced (in a quote too) and indented code, inline code spans, `<code>` and `<tt>` elements, and HTML comments. An indented block that holds `[[` gets a `REVIEW` line, since list continuation and code look alike there. A code span that runs across lines isn't seen as code, so check any `REVIEW` line that quotes shell syntax (`[[ -f x ]]`).\n\n- **Inside an HTML block** (a line opening with `<div>`, `<p>`, `<table>`, `<details>`, and the like, up to the next blank line), Markdown shows as text, but gollum, the engine behind GitHub's wikis, resolves `[[…]]` before the Markdown is parsed, so it made a link there: `convert` writes `<a href=\"/page-name\">text</a>` there, and a bare wiki URL too. An image tag there gets a `REVIEW` line: write it as `<img src=\"…\" />` with the relative path the line gives (Blume publishes the file with the page), or move it out of the block. A `#` line inside the block is text, not a heading, so it isn't demoted.\n- **`'[[Page]]`** (an apostrophe right before, none right after) is gollum's escape for literal brackets: `convert` leaves it and reports it. Check the live page, and drop the apostrophe if it showed the brackets.\n- **`\\[\\[Page\\]\\]`** converts as a wiki link, because pandoc writes them that way (Other markup formats), with a `REVIEW` line: on a page that was always Markdown, GitHub showed those brackets, so restore them there.\n\n| GitHub (`.md` pages) | Blume |\n| --- | --- |\n| `[[Page Name]]` | `[Page Name](/page-name)`: the text as written, the route from the table |\n| `[[Link text\\|Page Name]]` | `[Link text](/page-name)` |\n| `[[Page Name#anchor]]`, `[[text\\|Page Name#anchor]]` | `[…](/page-name#anchor)`, the anchor as written |\n| `[[text\\|https://…]]` | `[text](https://…)` |\n| `[[Home]]` | `[Home](/)` |\n| a page the wiki doesn't have (GitHub drew it as a link to create the page), or one set to `null` | plain text; reported |\n| a moved notice | a link to its target |\n| `[[image.png]]`, `[[/images/x.png\\|alt=Text]]`, `[[x.png\\|align=center,frame,alt=Text]]`, `[[https://…/x.png\\|alt=Text]]` | an image: `![Text](…)` (Images and files). Any first part with an image extension is an image, whatever follows; options are separated by `,` or `\\|`. A leading `/` is the wiki root. A path the root doesn't have but the page's own folder does resolves there, reported. Options other than `alt=` (`width=`, `align=`, `frame`, stray text) drop; reported |\n| `[[text\\|docs/spec.pdf]]` (a file stored in the wiki) | left as is and reported: copy the file into `public/` and link its root path |\n| `[[_TOC_]]` | deleted: GitHub lists a table of contents among unsupported syntax, and Blume's outline shows the headings |\n\n(In this table `\\|` is the table's escape for a single pipe.) GitHub trims spaces around the pipe (`[[Mailing list | https://…]]`), and inside a Markdown table cell the pipe may be written `\\|`. Pages are looked up the way GitHub did: by name, ignoring case, with `-` and space interchangeable and folders ignored. **Anchors are case-sensitive in Blume** and weren't on GitHub, wherever they appear: when `blume validate` reports one (`#TSDeclFiles` for a heading pinned `[#TsDeclFiles]`), match the id's case.\n\n### Markdown links\n\n| Link | Blume |\n| --- | --- |\n| `https://github.com/<owner>/<repo>/wiki/Page-Name` (any case of owner and repo, `http`, `www.`), `/<owner>/<repo>/wiki/Page-Name`, or bare `Page-Name` / `./Page-Name` | the page's route through the table, keeping `#anchor`, percent-decoded (`%28` is `(`) |\n| `…/wiki`, `…/wiki/`, `…/wiki/Home` | `/` |\n| a mirror repository's copy of a page (`https://github.com/<owner>/<repo>-wiki/blob/<branch>/Page.md`) | the page's route (`--mirror`): the mirror goes stale with the wiki |\n| a bare wiki URL in prose, or `<https://…/wiki/Page>` | `[Page title](/route)` |\n| a page's file by its path (`dir/Page.md`, `…/wiki/dir/Page.md`), which GitHub served raw | the page's route; reported |\n| `#user-content-x` | `#x` (GitHub's ids carry that prefix, so both forms worked there) |\n| relative links that leave the wiki (`../issues/12`, `../blob/main/src/x.ts`, `../../other-repo`) | the absolute URL each resolved to from `https://github.com/<owner>/<repo>/wiki/<page>` (`../issues/12` → `https://github.com/<owner>/<repo>/issues/12`) |\n| a link to a page that doesn't exist (`./formatter.md`, `GLOSSARY.md#x`: GitHub served nothing there) | left as is and reported: it was broken on GitHub too, so retarget it (often the page moved to another repository) or unlink it |\n| `…/wiki/_pages`, `…/wiki/<Page>/_history`, revision URLs | left and reported: they point at the wiki's UI and stop working with it |\n| another repository's wiki | unchanged |\n\n`#123` and `@user` are plain text in wikis (GitHub doesn't autolink them there), so they stay. Bare URLs autolink in both. A raw HTML `<a href>` to a wiki page gets the route too (and a wiki link in an HTML block becomes one); Blume adds `deployment.base` to its root path like a Markdown link's.\n\n### Link definitions\n\n`blume validate` checks every link reference definition the page uses, including ones nothing cites; when a label is defined twice it checks only the first, which CommonMark uses. Wikis kept by scripts collect definitions that never rendered:\n\n- **A later definition of a label** never rendered: `convert` deletes it.\n- **A Foam or VS Code block** of autogenerated definitions (`[//begin]: # \"Autogenerated link references…\"` … `[//end]: #`) goes too: once the wiki links are converted, nothing uses it.\n- **A line like `[1]: <src/file.ts - function x(`** with no closing `>` isn't a definition at all: it shows as text, on GitHub and in Blume alike. Leave it, or delete it if it's leftover tooling syntax, and report it.\n\n### GitHub Markdown\n\n| GitHub | Blume |\n| --- | --- |\n| `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` at the start of a line | `:::note`, `:::tip`, `:::note`, `:::warning`, `:::danger`, in `.mdx`. Blume renders an alert itself as the same callout in `.mdx`, so `convert` leaves one indented in a list, or with lazy lines, as written and makes the page `.mdx`. One with text after its marker, or no body, is reported: convert it by hand. In a `.md` page an alert renders as a quote with a literal `[!NOTE]` and warns `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. The older `> **Note**` form is a plain quote on GitHub today: leave it |\n| ` ```mermaid ` | the same fence, in `.mdx` |\n| ` ```math ` | a `$$` block, in `.mdx` |\n| `$…$`, ``$`…`$``, `$$…$$` | by hand: `$$…$$` in `.mdx` (inline ones stay in their sentence; SKILL.md \"Math\") |\n| ` ```geojson `, `topojson`, `stl` | a code block: maps and 3D models drop; reported |\n| Fence languages | lowercased (`TypeScript` → `typescript`), which only tidies: Blume reads them in any case, as GitHub did. `posh` → `powershell`. Any other name Shiki lacks renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: change it to a Shiki language, or `text` |\n| `:emoji:` shortcodes | the character, with `--emoji`. GitHub slugged a heading's shortcode name, so a heading with one is reported: convert it and pin the old id (`## ⚠️ Known issues [#warning-known-issues]`) |\n| `<a name=\"x\"></a>` in a heading | deleted, with `[#x]` pinned on the heading (Blume would give the heading the id `x` anyway). Elsewhere it stays: links to `#x` work, and `blume validate` resolves them |\n| task lists, tables, footnotes, strikethrough, autolinks, `<details>` | unchanged |\n| `style=`, `<script>`, `<iframe>`, `<form>`, `<input>` | by hand: GitHub's sanitizer removed them, so readers never saw them, while Blume renders raw HTML as written. Delete them, or keep one deliberately and report it |\n\nHeading ids need no pinning beyond the rows above: GitHub and Blume both slug with `github-slugger` (identical ids for all 710 headings on the wiki pages checked). The exception is a heading that ends in an emoji or symbol after a space: GitHub keeps the trailing dash (`## Features ✨` → `#features-`), Blume drops it (`#features`); `blume validate` reports the links to fix. Deleting a leading H1 can shift the `-1` suffix of a later heading with the same text; `blume validate` reports the links that break. `pin-heading-ids.mjs` can't read a wiki anyway: GitHub puts each id on a link beside the heading, not on the heading.\n\n### MDX hazards\n\nMDX rejects things GitHub's Markdown accepts, and a page renamed to `.mdx` fails `blume build` on them. In prose (outside code), `convert` fixes the mechanical ones in the pages it renames: a `<` that doesn't start a tag (`TypeScript >=1.6, <7` → `&lt;7`), bare `{` and `}` (escaped `\\{`), HTML comments (`{/* */}` on one line), unclosed void tags (`<br>` → `<br />`), `<https://…>` autolinks, and indented code (MDX has none, so a top-level indented block gets a fence; a backslash-escaped backtick doesn't start inline code, so braces after it get escaped too). It reports the rest:\n\n- **A tag-like word** (`Array<string>` outside code, `<placeholder>`): MDX reads it as an unclosed JSX element. Put it in backticks.\n- **Raw HTML blocks** (`<details>`, `<div>`, `<table>`) with Markdown inside: MDX parses that Markdown only with blank lines around it, and fails on tags interleaved with list items. Add the blank lines, or turn the block into Markdown.\n- **Indented code inside a list:** fence it by hand, at the list item's indentation.\n\n### Other markup formats\n\nConvert each non-Markdown page to Markdown before `convert` (which skips and lists them): `pandoc -f <format> -t gfm-raw_html --wrap=preserve page.<ext> -o page.md` (`mediawiki`, `textile`, `rst`, `org`, `creole`, `asciidoc`, `pod`). `.rdoc` has no pandoc reader: convert it by hand. Delete the original, point the page's entry in `wiki-files.json` at the new `.md` file, and rerun `convert`.\n\n- **MediaWiki pages put the target first,** `[[Page Name|text]]`, as MediaWiki does. Pandoc turns them into `[text](Page_Name \"text\")`, with `_` for spaces, and `convert` maps those to routes.\n- **In the other formats** pandoc escapes `[[…]]` to `\\[\\[…\\]\\]`, and `convert` reads those as Markdown wiki links (text first).\n- Heading ids differ in these formats. Compare each converted page with the live one, and list every conversion in the report.\n\n## Frontmatter\n\nGitHub reads no frontmatter, so wiki pages have none. Each page gets `title`, plus `sidebar.label` and `slug` where the sections above say so. If a page opens with a `---` block anyway, check the live page for what readers saw, keep that as content or drop it, and report it.\n\n## Images and files\n\n- **Files stored in the wiki** (`https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`, `https://github.com/<owner>/<repo>/wiki/<path>`, or a relative `<path>`, which resolves from the wiki root whatever the page's folder) go away with the wiki. `convert` moves each referenced one into `docs/` at the same path (`docs/images/x.png`) and points the page at it relatively (`../images/x.png` from a `(group)/` page): Blume optimizes and validates relative Markdown images.\n- **A relative image that pointed at nothing** (it climbs out of the wiki root, like `../../screenshots/x.png`, or names a file the wiki doesn't have) was broken on GitHub too, and fails `blume build`: `convert` drops it and reports it.\n- **Uploaded attachments** (`user-images.githubusercontent.com`, `github.com/user-attachments/assets/…`, `private-user-images.githubusercontent.com`) are served outside the wiki and keep working for a public repository. `convert` reports each one: download and vendor it next to the pages (one request at a time) unless the user objects. From a private or internal repository they need a GitHub login, so vendoring is required.\n- **`https://github.com/<owner>/<repo>/blob/<ref>/<path>`** shown as an image is an HTML page anywhere but GitHub: `convert` uses `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`.\n- **Raw `<img>` alone on its line** becomes a Markdown image; its `width`/`height`/`align` drop and are reported. A raw `<img>` of a wiki file inside other HTML (a table cell, a `<p>`) is reported: its `src` still reads from the wiki root, which isn't the page's new folder. Copy the file into `docs/` at the same path, as `convert` does for the others, and point `src` at it relative to the page (`../images/x.png`), which Blume publishes with the page, unoptimized; or make it a Markdown image.\n- **Linked files** (PDFs, archives, an image opened as a link) are reported: copy each into `docs/` at the same path and link it relative to the page, which publishes it with the page, or into `public/` and link its root path for a URL that never changes.\n- `#gh-dark-mode-only` / `#gh-light-mode-only` pairs: keep the light image (report).\n- Files nothing references stay in the wiki (other sites may hotlink them): report them, don't copy them.\n\n## URLs and the old wiki\n\nEvery old URL is on github.com. GitHub has no redirect setting for wiki pages, and it sanitizes their HTML (no `<script>`, no `<meta>` refresh), so nothing on the old wiki can forward a reader. Don't add Blume `redirects` for them either: they would only match paths on the new site. The route table is the record instead: each old page name maps to its new route, a target URL, or `null`.\n\n**Check the table against the build** after `blume build`: `node <skill>/scripts/github-wiki-codemod.mjs check` confirms every route in the table exists in `dist/` (`dist/client/` on a server build), anchors included. Given the old URLs instead (every line of a saved `_pages` list, for one), it prints each one's new route: `ok`, `moved` for a moved notice's outside target, `DROPPED` for a page set to `null`, or `MISSING` for a page the table lacks.\n\n**What the user can do on the old wiki** (prepare it, but never commit or push it, even with push access: `stubs` only writes files, and whether and when anything goes to `<repo>.wiki.git` is the user's decision, after the new site is live):\n\n1. **Restrict editing** (Settings → Features, under Wikis: **Restrict editing to collaborators only**) as soon as the migration starts, so the content stops drifting.\n2. **Stop anything that syncs the wiki, before pushing anything to it.** Otherwise it reverts the stubs or copies them on. Look for a workflow triggered `on: gollum` (wiki edits) in the main repository, a mirror repository (`<repo>-wiki`) whose workflow pushes to `<repo>.wiki.git` on push or on a schedule, a copy of that workflow inside the wiki itself, and wiki-publishing actions. List every one you find.\n3. **Point readers at the new site:** a notice at the top of `Home` and `_Sidebar.md` (the sidebar shows on every page). In a fresh clone of the wiki (not the Blume project: `stubs` refuses to run beside `blume.config.ts`), with `wiki-routes.json` and `wiki-files.json` copied in (don't commit them there): `node <skill>/scripts/github-wiki-codemod.mjs stubs --site https://docs.example.com --notice-only --write`.\n4. **Or replace every page with a link stub,** so deep links from issues, search results, and bookmarks land one click from the new page: the same command without `--notice-only`. Each page keeps its file name, so its URL keeps working; `_Footer` files go, `_Sidebar.md` becomes the notice, images stay for sites that hotlink them, and pages set to `null` are listed for the user to decide. `--site` is the new site's URL with its base path, so regenerate the stubs once the real URL is known.\n5. **Disable the wiki** (Settings → Features → Wikis), once the repository's own links point at the new site. GitHub hides every page, stubs included, without erasing them. It's the user's call; it's never required.\n\nWhether search engines matter: `curl -sI https://github.com/<owner>/<repo>/wiki | grep -i x-robots-tag` prints `none` for a wiki GitHub keeps out of search results (GitHub indexes only wikis with 500+ stars and editing restricted to collaborators). Without it, search results keep sending readers to the old pages for a while, and the stubs are what they find.\n\n**Inbound links in the main repository.** Search its clone with `git grep -n -i -E 'github\\.com/<owner>/<repo>/wiki'`: README, CONTRIBUTING, `docs/`, `.github/ISSUE_TEMPLATE/`, `SECURITY.md`, code comments and test fixtures, `package.json` `homepage`. A large repository doesn't need a full clone: `git clone --depth 1 --filter=blob:none --sparse <url>`, then `git sparse-checkout set --no-cone '/*' '!/tests/baselines/'` (excluding its biggest generated folders). Map each URL through the table with `check`, which also verifies its anchor against the build and suggests ids for one that's gone:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs check https://github.com/<owner>/<repo>/wiki/FAQ#common-bugs\n# MISSING ANCHOR  …/wiki/FAQ#common-bugs → /faq#common-bugs (ids like: common-bugs-that-arent-bugs)\n```\n\nAn old link can carry an anchor that was already dead on GitHub: fix it to a real id, don't copy it. Rewrite each link with the new site's origin (or ask first, when the user only wants a report), and set the repository's About → Website to the new site if it pointed at the wiki. Links in issues, pull requests, and other repositories can't be rewritten: the stubs catch them. List the files you changed or would change.\n\n### Leftover check\n\nRerun until it prints only code samples you mean to keep:\n\n```bash\ngrep -rnE '\\[\\[|user-content-|(^|\\s):[a-z0-9_+][a-z0-9_+-]*:(\\s|$)' docs\ngrep -rniE 'raw\\.githubusercontent\\.com/wiki/|github\\.com/<owner>/<repo>/wiki' docs   # any case of owner and repo\n```\n\nAn alert left in a `.md` page warns `BLUME_MD_GITHUB_ALERT`, and an unknown fence language `BLUME_UNKNOWN_CODE_LANGUAGE`, so `blume build` lists those.\n\n## Teardown\n\n- **Files left behind.** Converting in place leaves the pages you didn't keep where they were: moved notices, `null` pages, the files GitHub never served, and unreferenced images. `convert` lists them. Delete them from the project (nothing under the root publishes, but they confuse later edits): the wiki keeps them, and `wiki-files.json` records their paths for the stubs. Delete `_Sidebar.md` and `_Footer.md` once `convert` has harvested them (a later `convert` then leaves the `meta.ts` files as they are).\n- **Wiki tooling** (a `package.json` for a Markdown generator, link-rewriting scripts, sync scripts and workflows) stays only if it still applies to the pages: run each script in its dry or check mode against `docs/` and see whether any page still uses the syntax it converts. If none does, it goes, with the dependencies only it used. Say which you kept.\n- **Lockfiles:** a mirror repository's `.gitignore` often ignores `package-lock.json` or `yarn.lock`. Remove those lines, so the lockfile SKILL.md step 6 regenerates gets committed.\n- Keep `wiki-routes.json` and `wiki-files.json` beside `blume.config.ts` until the stubs are pushed and the inbound links are rewritten.\n- Scaffold `package.json` (SKILL.md step 6) if the docs' new home has none.\n\n## Dropped — report these\n\nThe Pages list and the clone box; footer text other than a copyright line, and its link; sidebar prose, images, anchor links, and duplicate entries; external sidebar links moved to `navigation.featured`; the per-page \"edited … · N revisions\" line; image sizes and wiki image options; images that were broken on GitHub; maps and 3D models; theme-aware image pairs; raw HTML GitHub had stripped; links to missing pages (GitHub's create-page links) and links that were already broken; shadowed link definitions; pages left out (mirror files, tests, fixtures, deprecated stubs) and moved notices mapped to their targets; pages GitHub never served (same-name shadows); non-Markdown pages converted with pandoc; wiki-UI links (history, revisions, `_pages`); and every old URL, which stays on github.com: say what you prepared for it (notice, stubs, or neither), which sync workflows must stop first, and that nothing redirects.\n","isInternal":false,"tokens":9187,"sizeBytes":35469},{"name":"jekyll.md","path":".agents/skills/blume-migrate/references/jekyll.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/jekyll.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Jekyll (Just the Docs) → Blume\n\nJekyll is a Ruby static site generator: a `_config.yml`, Markdown pages with front matter, Liquid templating (`{% … %}`, `{{ … }}`) in every page, and Kramdown as the Markdown engine. Docs sites get their navigation from a theme, and the common docs theme is **Just the Docs** (JTD). The work is turning JTD's front-matter navigation into folders and `meta.ts` without moving a URL, and converting the Kramdown and Liquid in every page. Watch for six traps:\n\n- **Navigation lives in front matter** (`parent`, `nav_order`), not in folders. URLs come from the file path or `permalink`, so the nav tree and the file tree often disagree.\n- **Kramdown attribute lists (`{: .note }`) and Liquid (`{% include %}`, `{{ site.x }}`) render as literal text in a Blume `.md` page, with a green build.** Each warns at its line (`BLUME_TEMPLATE_TAG` for Liquid, `BLUME_MD_ATTRIBUTE_LIST` for an IAL). In `.mdx` both fail the build, and `blume check` names each line first: `BLUME_MDX_SYNTAX` for Liquid, `BLUME_MDX_ATTRIBUTE_LIST` for an IAL. Callouts are IALs, so most pages need converting.\n- **URLs end in a slash** (`permalink: pretty`, `/a/b/`) or in `.html` (the default), and relative links were written against those URLs.\n- **The page heading is the body `# H1`.** Front matter `title` is the nav label and `<title>`.\n- **Callout names are the site's own**, declared in `_config.yml`. An IAL whose name isn't declared, or is misspelled, rendered a plain paragraph.\n- **What a page shows depends on Liquid and CSS that ran around it** (`jekyll.environment`, `{% if %}`, `_data`, rules in `_sass/custom` that hide sections or add text). The built site is the record of what readers saw.\n\nScope: **Jekyll 4.x with Just the Docs** (gem or `remote_theme`) is supported, and so is a JTD site that GitHub Pages builds with its own Jekyll 3 (differences flagged below). Checked against Jekyll 4.4.1, Just the Docs 0.12.0 (navigation as of 0.10: unlimited depth, `ancestor`), kramdown 2.5.2 with kramdown-parser-gfm 1.1.0, jekyll-redirect-from 0.17.0, GitHub Pages' gem set (`github-pages` 232, Jekyll 3.10.0), and an end-to-end migration of artineering-io/flair-docs (61 pages). **Plain Jekyll and other doc themes are manual** (see the end): the codemod's Kramdown, Liquid, link, and redirect passes apply to any Jekyll site; only the navigation is JTD's.\n\n## Detect\n\n- **`_config.yml`** at the root or in `docs/` (GitHub Pages' \"deploy from `/docs`\"). It's generic Jekyll.\n- **Just the Docs:** `theme: just-the-docs` or `remote_theme: just-the-docs/just-the-docs` (also the old `pmarsceill/just-the-docs`, with or without `@ref`) in `_config.yml`, or `gem \"just-the-docs\"` in the `Gemfile`. Pages carry `nav_order`/`parent`.\n- **Not Jekyll:** a `_toc.yml` beside `_config.yml` is Jupyter Book; `hexo` in `package.json` is Hexo. A `_config.yml` holding only `theme: jekyll-theme-*` publishes a repo's README with GitHub Pages: one page.\n- **Which Jekyll:** a `Gemfile` with `gem \"jekyll\", \"~> 4…\"` is 4.x. `gem \"github-pages\"`, or no `Gemfile` with a `remote_theme`, is GitHub Pages' Jekyll 3 (3.10 today), which also forces nine plugins on (`jekyll-optional-front-matter`, `-readme-index`, `-titles-from-headings`, `-relative-links`, `-default-layout`, `-github-metadata`, `-gist`, `-paginate`, `-coffeescript`): front-matterless Markdown files are pages, `README.md` is its folder's index when there's no `index.md`, an untitled page takes its first heading as `title`, and `[x](page.md)` links work.\n\n**Where the Blume project goes:** in the Jekyll source directory (the folder holding `_config.yml`), beside `_includes/` and the pages. Set `content.root: \".\"` and scope `content.include` to the page files (`[\"*.{md,mdx}\", \"docs/**/*.{md,mdx}\"]`), and list Jekyll's own `exclude` Markdown (`README.md`, `CHANGELOG.md`) plus `vendor/**` and `node_modules/**` in `content.exclude`, which adds to the defaults `\"**/_*\"` and `\"**/.*\"` (`references/monorepo.md` §1). `_includes/` and `_data/` stay unpublished because of their underscore. In a code repo whose Jekyll source is `docs/`, that puts `package.json` in `docs/` and keeps `dist/` out of the repo root.\n\n## Build the old site first\n\nThe old build is your URL list, your nav tree, your heading ids, and the record of what rendered. Build a copy in a folder outside the repo (`<old>` below, an absolute path), with a project-local bundle. CI usually sets `JEKYLL_ENV=production`, which switches analytics and any `jekyll.environment` branches in includes. From the Jekyll source directory:\n\n```bash\nrsync -a --exclude .git --exclude _site ./ <old>/src/ && cd <old>/src\nbundle config set --local path vendor/bundle && bundle install\nJEKYLL_ENV=production bundle exec jekyll build -d <old>/site\ncd <old>/site && find . -name '*.html' -not -path './assets/*' | sed 's|^\\.||; s|/index\\.html$|/|' | sort > <old>/old-urls.txt\n```\n\nNo `Gemfile` (a GitHub Pages `remote_theme` site): write one with `gem \"github-pages\", group: :jekyll_plugins`. Recent Rubies moved `csv`, `base64`, `bigdecimal`, and `logger` out of the default gems, so under Bundler an older Jekyll fails to load them: add the one it names to the copy's `Gemfile`. If no build works, use the live site (`pin-heading-ids.mjs --old <url>`) and its `sitemap.xml`, rebuild the nav by hand, and say so.\n\nIn `old-urls.txt`, a slashed entry is a page. An `.html` entry is a page on a non-`pretty` site, `404.html`, or a jekyll-redirect-from stub, which GitHub Pages serves without the `.html` (`/old` from `old.html`): `redirects.json` in the build lists those. Non-page files Jekyll copied (`LICENSE`, `.bat` scripts, a `Rakefile`) were published by accident unless a page links them: leave them out and report them. On Jekyll 4, a `.md` file with no front matter wasn't a page either: Jekyll copied it raw.\n\n## Run the codemod\n\nFrom the Jekyll source directory, dry-run the bundled codemod against the old build, then apply it once, before any hand edits:\n\n```bash\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site .          # report only\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site --write .\n```\n\nIt's zero-dependency and deterministic, and it runs once: `--write` always writes `jekyll-migration.json` and refuses a folder that already has one, because a second pass would take each page's next H1 as its title and convert what `{% raw %}` protected. To run it again, start from a fresh copy of the Jekyll source. It reads `_config.yml` (`callouts`, `url`, `baseurl`, `permalink`, `defaults` permalinks, `exclude`, plain values for `{{ site.x }}`) and every page and Markdown partial in `_includes/`, and converts what maps exactly:\n\n- callout IALs → directives (Callouts below), `.label` → `<Badge>`, `.no_toc` and `{#id}` on headings → `[!toc]` and `[#id]`, and JTD's utility and button classes dropped; a malformed IAL, or one naming a class `callouts:` doesn't declare, rendered a plain block, so it goes and is reported;\n- the in-page TOC (the list, its `<details>`, its heading, a partial holding only a TOC), `{::comment}`, ALDs;\n- the body H1 → `title` (a different old `title` → `sidebar.label` and `seo.title`), with a variable it showed written in as its value, since Blume leaves front matter as written; an H1 inside an HTML comment or `<pre>` is skipped. The front matter keys follow the table below;\n- `{% include x.md k=\"v\" %}` and `{% include_relative %}` → `<include k=\"v\">/_includes/x.md</include>` (nested ones too, `{{ include.k }}` → `{{k}}`); a parameter the partial reads but the tag leaves out printed nothing in Jekyll, so it's passed as `k=\"\"`. Also `{{ site.x }}` → `{{x}}` variables, `{{ site.baseurl }}`, `relative_url`, `absolute_url`, `{% link %}`, `{% highlight %}`, `{% comment %}`, and `{% raw %}` (unwrapped, its content untouched);\n- links: relative links resolved against each page's old URL, `.md`, `.html`, own-origin, and trailing-slash links → routes; relative Markdown images rebased from the page's new folder, and a relative raw HTML `src` → a root path (Jekyll resolved it against the page's old URL, which the new folders don't mirror);\n- with `--old`, the sidebar the old build rendered → folders, `slug`s, and `meta.ts` files (Navigation below);\n- each page that now needs MDX → `.mdx`, with void tags self-closed, comments as `{/* */}`, a bare `<` as `&lt;`, autolinks as links, and page `<script>`/`<style>` removed, inline code left as written; partials such a page includes get the same;\n- `redirect_from`, `redirect_to`, `.html` URLs, and the variables → `jekyll-migration.json`, for the config:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport migration from \"./jekyll-migration.json\" with { type: \"json\" };\n\nexport default defineConfig({\n  redirects: migration.redirects,\n  variables: migration.variables,\n});\n```\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each: HTML includes, Liquid logic, unknown front matter, MDX hazards (a brace in prose, an indented code block), the CSS rules that hid or added content, and the theme files. A `{{ name }}` that `{% raw %}` kept as written collides with a variable of the same name, which Blume fills in everywhere, code included: rename the variable (its key in `jekyll-migration.json` and the `{{name}}` references the codemod wrote). A site whose `_config.yml` has no `callouts:` (before JTD 0.4 added the option, sites styled `.note`/`.warning` in `_sass/custom/custom.scss`) keeps its callout IALs, and the dry run says so: read those rules for the names, then pass `--callouts note,warning` on the write run.\n\nAfter the first `blume build`, pin the old heading ids, rebuilding until it reports 0. Jekyll's GFM ids drop underscores (`f_position` → `#fposition`) where Blume keeps them, so about one id in twenty differs:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old>/site --write\n```\n\n## Config: `_config.yml` → `blume.config.ts`\n\nApply `defaults:` (front matter defaults) to the pages in each `scope.path` that the codemod didn't see through: they often set `layout`, and sometimes `parent` or `nav_exclude`.\n\n| Jekyll | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `url` | `deployment.site`, unless the target is Vercel or Netlify (SKILL.md). GitHub Pages with a custom domain needs it |\n| `baseurl` (`/repo`) | `deployment: { base: \"/repo\" }`. **The deployed URL decides:** the GitHub Pages starter workflow passes `--baseurl` from the Pages settings, so a custom domain (a `CNAME`) serves at the root whatever `_config.yml` says |\n| `exclude` / `include` | `content.exclude` / `content.include` (Detect) |\n| `permalink`, `defaults`, `collections` | URLs; above; Collections and posts |\n| `plugins` | Plugins |\n| `kramdown.syntax_highlighter_opts.block.line_numbers: true` | no site-wide switch: drop it and report it. Code without line numbers copies cleanly |\n| `kramdown.toc_levels`, and pages built on H4 sections | Blume's outline shows H2–H3, where Kramdown's TOC listed every level. Count the H4s: when they carry the page (attribute references), set `toc: { maxHeadingLevel: 4 }`; else report it |\n| other `markdown`, `kramdown`, `highlighter`, `sass`, `compress_html`, `liquid` keys | drop |\n| `github_username`, `twitter`, `social.links`, a Discord link | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube) |\n| `repository` (jekyll-github-metadata) | `github: { owner, repo }` |\n| `logo` | `logo`, file moved to `public/`. JTD shows the logo _instead of_ the title, so a wordmark needs `logo: { image, text: \"\" }`; a custom `_includes/title.html` that shows both keeps the text |\n| `favicon_ico` | the file at `public/favicon.ico` (no config field) |\n| `color_scheme: dark` | `theme.mode: \"dark\"`. `light`/unset → omit. A custom scheme (`_sass/color_schemes/<name>.scss`): its `$link-color` → `theme.accent` (`$blue-000` is `#2c84fa`; look the rest up in the theme's `_sass/support/_variables.scss`), and an `@import \"./color_schemes/dark\"` in it → `theme.mode: \"dark\"` |\n| `aux_links` (`\"Label\": \"url\"` or `[\"url\"]`) | `navigation.actions: [{ label, href }]`. `aux_links_new_tab` → drop (Blume opens `http(s)` header links in a new tab) |\n| `nav_external_links` (`title`, `url`) | `navigation.featured` (above the sidebar, where JTD listed them below the pages) |\n| `search_enabled: false` | `search: false`. `search.*` options → drop |\n| `nav_enabled: false` | no site-wide switch: `mode: center` on the pages that showed no sidebar |\n| `mermaid` | delete; ` ```mermaid ` renders natively in `.mdx` |\n| `ga_tracking` | `googleAnalytics({ id })` from `blume/analytics`, one per `G-` id in the comma list. A `UA-` id no longer collects data: report it |\n| `gh_edit_link` + `gh_edit_repository` + `gh_edit_branch` | `github: { owner, repo, branch }`. `gh_edit_source: docs` → `github.dir: \"docs\"` when `blume.config.ts` sits there |\n| `last_edit_timestamp` | `lastModified: \"frontmatter\"` when pages set `last_modified_date` (the codemod maps it), else drop: JTD showed no date without it |\n| `footer_content` / `_includes/footer_custom.html` | links → `footer.links`, social links → `footer.socials`; copyright text → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), and turn a Liquid year (`{{ \"now\" \\| date: \"%Y\" }}`) into `${new Date().getFullYear()}` in a template literal; other text → drop (report) or a `Footer` layout slot (`defineComponents`) |\n| `callouts`, `heading_anchors`, `enable_copy_code_button`, `back_to_top*`, `nav_sort`, `nav_error_report`, `ga_tracking_anonymize_ip`, `gh_edit_link_text`, `gh_edit_view_mode` | drop (the codemod read `callouts`) |\n\n## Navigation: front matter → folders and `meta.ts`\n\nJTD builds one tree from `title`, `parent`, `grand_parent`/`ancestor`, and `nav_order`, wherever the files sit, and shows it as a collapsible tree with only the current section open: set **`navigation.sidebar.display: \"group\"`**. The codemod rebuilds the tree from the sidebar the old build rendered, so `nav_order` ties, `child_nav_order: reversed`, and `nav_sort` are already resolved, and keeps every URL:\n\n- A parent at `dir/index.md` with its children in `dir/` (the layout JTD's docs recommend) moves nothing.\n- A parent that's a leaf file with its children beside it (`docs/setup.md`, `docs/install.md`) becomes a `(group)` folder: `docs/(setup)/index.md` with `slug: docs/setup`, and the children inside it keep their URLs.\n- A child whose file sits elsewhere moves under its parent with `slug: <old route>`. A child _section_ whose folder sits elsewhere is reported: `git mv` it into the parent's folder (into a `(group)` folder, its URLs stay; into a real one, pin each page's `slug`), or leave it. The parent's `meta.ts` already lists it by its folder name.\n- **A `parent` title that names two pages** (two sections both titled \"Setup\", told apart by neither `grand_parent` nor `ancestor`): JTD listed the child under both. Blume lists a page once, so it stays under the first and the codemod reports it.\n- Each folder gets a `meta.ts`: `pages` in the old order; `title` only where Blume's label for the folder (its name, humanized) differs from JTD's; `directory: \"accordion\"` where the old parent page listed its children (JTD's `has_toc`, read from the old build), and `\"none\"` below it where a page didn't, since `directory` inherits.\n- **The duplicate index row:** Blume links a folder's `index` from the group row and lists it again as the first row, where JTD showed it once. The codemod hides that row (`sidebar.hidden: true`); a page the group row links stays in the sitemap, search, and `llms.txt`. `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`, flags a hidden index whose `title` differs from its folder's `meta.ts` `title`, unless its `sidebar.label` names the folder title. An index whose H1 differed from its JTD title gets that title as `sidebar.label`, which is the section label, so it passes too. Where neither the page's `title` nor its `sidebar.label` matches the section label (JTD always labeled a section with its page's `title`, so this is rare), the codemod leaves the row visible and says so. The check accepts three ways out: match the two titles, set the page's `sidebar.label` to the folder title, or show the row (remove `sidebar.hidden`).\n- Pages outside the old sidebar (`nav_exclude`, no `title`, a `parent` that matched nothing) get `sidebar.hidden: true`, which also takes them out of search, the sitemap, and `llms.txt`, where JTD still searched them: set `search: { indexing: { includeHiddenPages: true } }` if they should stay searchable. A hidden home page is the exception: the root URL serves it, so it stays listed. An external `nav_external_links` entry is listed for `navigation.featured`.\n\nWhat's left for you:\n\n- **A wrapper folder JTD never showed** (the template's `docs/`, any folder whose pages sit at the nav's top level) becomes one group around them. Blume opens a group that's the sidebar's only top-level row by itself, but one beside the home page's row or another section starts closed off its own pages, so the codemod writes `collapsed: false` in its `meta.ts`. A `navigation.tabs` entry instead lifts its pages to the sidebar's top level, but there pages list above groups, so JTD's interleaved order breaks. Keep the group unless the user asks for a tab.\n- **The home page** (`index.md`, often `layout: home` and untitled) stays the content root's `index`. When the old nav never showed it, the codemod hides its row like any page outside the nav; a hidden home page stays in the sitemap, search, and `llms.txt`. Give it a `title` (Blume renders it as the H1, where JTD showed none) or `mode: frame` to keep the sidebar without one. Where the home page headed a JTD section, Blume's root index can't head a group: its children stay in their folders, and the codemod reports it.\n- **Moving a page changes what its relative images resolve against.** The codemod rebases the Markdown images it converted, and turns a relative raw HTML `src` into a root path, with a REVIEW to serve that file from `public/`.\n\n## URLs and redirects\n\nA Jekyll page's URL, for `dir/name.md`:\n\n| `permalink` (global) | `dir/name.md` | `dir/index.md` | Blume route |\n| --- | --- | --- | --- |\n| `pretty` | `/dir/name/` | `/dir/` | `/dir/name`, `/dir` |\n| unset (`date`), `none`, `ordinal` | `/dir/name.html` | `/dir/` | same |\n| a style ending in `/` / in `:output_ext` / neither | `/dir/name/` / `.html` / `/dir/name` (file `name.html`) | `/dir/` | same |\n\nA page's own `permalink:` wins, then one `defaults:` sets for its folder (the most specific `scope.path`), then the global style. The codemod applies all three, and with `--old` it checks each URL against the build: a page the build doesn't have at that URL gets a REVIEW and no `.html` redirect. Blume serves `<route>/index.html`, so `/dir/name/` keeps working on static hosts, and `blume dev` and `preview` redirect it to `/dir/name`.\n\n- **`.html` URLs** become one exact redirect each (a pattern can't strip a suffix). Never add one from `/dir/`, `/dir/index.html`, or `/index.html` (SKILL.md, Redirects).\n- **`redirect_from`** values become redirects as written (`/old` and `/old/` are different `from`s), keeping case (`/w/Main_Page`); one that is the page's own URL is skipped. **`redirect_to`** becomes a redirect from the page's route, and an empty page with it is deleted.\n- **Raw HTML** (`<a href>`, `<img src>`, `<video><source src>`) gains `deployment.base` on a root path, as a Markdown link does, so the root paths the codemod writes from relative links, `{{ site.baseurl }}`, `relative_url`, and `{% link %}` need nothing more. `blume validate` checks a raw `<a href>` against the routes and a media `src` against `public/`.\n- **Jekyll 3's `{% link %}`** doesn't add `baseurl` (Jekyll 4's does, so `{{ site.baseurl }}{% link … %}` doubled it there). The codemod drops both: Blume adds the base itself.\n\n## Content: what the codemod leaves\n\n**HTML includes** (`{% include x.html … %}`) are never spliced: `<include>` embeds a non-Markdown file as code. Replace each use and delete the partial when nothing uses it:\n\n- a figure → a Markdown image, or `<Frame caption=\"…\">` around it;\n- a YouTube iframe → `<YouTube url=\"…\" />`, inside `<Frame caption>` when the include printed a caption; a `width` → drop (report). A playlist URL (`/embed/videoseries?list=…`) works as written;\n- a row of embeds or code blocks in a flex `<div>` → `<Columns cols={2}>`;\n- a jQuery UI accordion (`$(\".accordion\").accordion()` over alternating question lines and answer `<div>`s) → `<Accordion>` with one `<AccordionItem title>` per question, the first `defaultOpen` as jQuery UI opened it; an `<details>` with Markdown in it → `<Expandable title>` (MDX wraps a `<summary>` and the text after it in one paragraph);\n- a note include (`note.html`, `callout.html`) → a directive.\n\n**Liquid logic** (`{% if %}`, `{% for %}`, `{% assign %}`, `{% capture %}`, a dynamic `{% include {{ var }} %}`, a parameter passed as a variable) → the output the old build shows: split a partial with `{% if include.x %}` into one partial per variant, write a `_data` loop's output once as Markdown, inline `{{ page.x }}`. A `{{ site.x }}` that isn't a plain value in `_config.yml` is left too. Liquid ran inside fenced code as well, unless wrapped in `{% raw %}`, so a REVIEW in a code block means the old page showed the rendered value.\n\n**CSS that changed content.** The codemod lists every `display: none` and `content: \"…\"` rule in `_sass/` and `assets/css/`. Find what each targets in the pages: content a rule hid wasn't shown, so delete it (and report the anchors that go with it); text a `::before`/`::after` added (`.label::before { content: \"New: \" }`) belongs in the converted element's text.\n\n**MDX hazards** the codemod lists in `.mdx` pages: a brace in prose (escape it `\\{` or put it in backticks), an indented code block (fence it), anything else the build names with a line. A partial with a hazard fails the build at the partial's own line (`blume check` warns `BLUME_MDX_SYNTAX` there first), and an unclosed `<img>` in a partial doesn't fail at all: it pulls the rest of the partial inside its parent element. The codemod self-closes the ones it sees, and `BLUME_MDX_UNCLOSED_ELEMENT` names the partial's line for any left.\n\n**A title in a partial:** a page that's only `{% include api/x.md %}` takes its heading from the partial's H1. Move it into the page's `title` (plain text: no `<kbd>` or backticks) and out of the partial, or Blume renders two.\n\n**Also by hand:** definition lists (`Term` + `: Definition` → a list or `<dl>`), `*[ABBR]:` abbreviations (delete), Font Awesome `<i class=\"fa…\">` icons (`<Icon icon>` with a Lucide name, or drop), and page scripts the codemod removed (an island, a `PageFooter` slot, or static content: two images instead of a before/after slider).\n\n### Callouts\n\nJTD has no built-in callout names: each site declares its own under `callouts:` with a `color` and an optional `title`, which JTD printed uppercase above every callout of that name. The codemod maps by the name's meaning, then by color:\n\n| JTD name (template color) | Blume |\n| --- | --- |\n| `note` (purple), `important` (blue) | `:::note`, `:::important` (an alias of note) |\n| `tip`, `hint` | `:::tip` |\n| `info`, `highlight` (yellow, untitled) | `:::info` |\n| `warning`, `attention`; `caution` | `:::warning`; `:::caution` (an alias of warning) |\n| `danger`; `error` | `:::danger`; `:::error` (an alias of danger) |\n| `new`, `success` (green) | `:::success` |\n| any other name | by its color: blue → `info`, green → `success`, yellow → `warning`, red → `danger`, purple or grey → `note` |\n\nBlume shows a type's icon, not its name, so a configured `title` that only names the directive is dropped and one that says more is kept (`new` titled \"New\" → `:::success[New]`). `{: .note-title }` takes its title from the blockquote's first paragraph. A callout class on a list, table, or code block rendered no box in JTD (it styles only paragraphs and blockquotes), so it's dropped. The codemod applies each IAL to the block Kramdown did: a list or blockquote that follows a paragraph without a blank line is its own block, and an IAL with blank lines on both sides applies to the next block. An IAL indented under a list item sat inside the item, where Kramdown sometimes boxed the item's paragraph: those are reported, so check the old page and write the callout inside the item by hand.\n\n## Front matter\n\nThe codemod maps these, and reports every other key Blume's strict schema rejects:\n\n| Jekyll / JTD | Blume |\n| --- | --- |\n| `title` + body H1 | `title` = the H1; a different old title → `sidebar.label` and `seo.title` |\n| `description`, or `summary` (JTD showed it in child lists) | `description`. JTD only put it in `<meta>`; Blume shows it under the title (report) |\n| `layout: default` / `page` / `home` / `post` / `about` | removed |\n| `layout: minimal`, `nav_enabled: false` | `mode: center` |\n| a custom `layout` from `_layouts/` | reported: pick a `mode` or a custom `.astro` page |\n| JTD nav keys, `permalink`, `redirect_from`, `redirect_to` | removed (Navigation, URLs) |\n| `search_exclude: true` | `search: { exclude: true }` |\n| `last_modified_date` | `lastModified` (ISO date) |\n| `published: false` | `draft: true` |\n| `image` (a string, or `{ path }`), `canonical_url` | `seo.image`, `seo.canonical` |\n| jekyll-seo-tag's `seo:` `type`/`name`/`links` | removed |\n| `sitemap: false` | reported: `seo: { noindex: true }` if it shouldn't be indexed, else delete |\n| `author`, `date`, `lang`, `tags`, `categories`, `excerpt`, `toc` | reported: delete (`tags` → `search.tags` if a hosted search filters on them) |\n\n## Theme, layouts, and assets\n\n- **`_sass/custom/custom.scss`** → a project-root `theme.css` with only the rules whose markup still exists (figure classes, flex rows): JTD selectors (`.main-content`, `.site-nav`, `.site-title`) match nothing in Blume. Turn hard-coded colors into Blume's tokens.\n- **Theme hook includes** (the codemod lists them): `head_custom.html` (favicons → `public/`, `<meta>` → `seo.metatags`, analytics → adapters, icon-font CSS → drop), `header_custom.html` (an announcement → `banner`), `footer_custom.html` (Config), `title.html` (→ `logo`), `nav_footer_custom.html`, `search_placeholder_custom.html`, `toc_heading_custom.html`, `mermaid_config.js`, `js/custom.js`, `lunr/*` → drop and report what each did.\n- **`_layouts/`** and `.html` pages with front matter → `.mdx` (with `mode: custom` for a landing page) or a custom `.astro` page. `404.html`/`404.md` → delete (Blume ships one).\n- **Assets:** a folder the old site served by root path (`/media/…`, `/assets/images/…`) moves whole to `public/` at the same path. Keep files no page references: other sites may link them. List them, and prune only once the user confirms. JTD's own `assets/css` and `assets/js` go.\n- **Root files** (`favicon.ico`, `apple-touch-icon.png`, `site.webmanifest`, `.well-known/`) → `public/`. A `robots.txt` replaces Blume's generated one: keep it only if it says something Blume's doesn't. `CNAME` and `.nojekyll` aren't needed with a GitHub Actions deploy.\n\n## Collections and posts\n\n- **A JTD collection** (`collections: { docs: { output: true, permalink: \"/:collection/:path/\" } }` plus `just_the_docs.collections.docs.name`): rename `_docs/` → `docs/` so the URLs stay `/docs/…`, then run the codemod; give the folder a `meta.ts` `title` from `name`. `nav_fold: true` → `collapsed: true`. A collection document without `title` took its filename as one: write it in.\n- **`_posts/`** → `type: blog` pages (`docs/advanced/blog.mdx`), with `slug` pinned to the old dated URL (`/:categories/:year/:month/:day/:title.html` by default, so also an `.html` redirect) or redirected; `{% post_url %}` → the post's route; jekyll-feed's `/feed.xml` → a redirect to `/blog/rss.xml`.\n\n## Plugins\n\n| Plugin | Blume |\n| --- | --- |\n| `jekyll-redirect-from` | `redirects` (the codemod) |\n| `jekyll-seo-tag`, `jekyll-sitemap` | built in (the sitemap needs `deployment.site`) |\n| `jekyll-relative-links` | built in: the codemod turns `.md` links into routes |\n| `jekyll-include-cache` (`include_cached`) | same as `include` |\n| `jekyll-remote-theme`, `jekyll-default-layout`, `jekyll-optional-front-matter`, `jekyll-titles-from-headings`, `jekyll-readme-index` | delete; the old build shows the pages and titles they made |\n| `jekyll-github-metadata` (`site.github.*`) | inline the values; `repository` → `github` |\n| `jekyll-last-modified-at` | `lastModified: \"git\"` |\n| `jemoji` (`:smile:`), `jekyll-mentions` (`@user`), `jekyll-gist` | paste the emoji; write the links; a link or a fence |\n| `jekyll-paginate`, `jekyll-coffeescript`, `jekyll-sass-converter`, `jekyll-compose` | drop |\n| anything else (`_plugins/*.rb` included) | read what it does; report it |\n\n## Plain Jekyll and other themes: manual\n\nRun the codemod without `--old` (or with it, for the URLs): its content and link passes apply to any Jekyll site. The navigation source differs, and there's no tested mapping for it:\n\n- **Plain Jekyll / minima:** `header_pages` (a list of files) or no nav. Lay out folders by topic and report the new sidebar.\n- **Minimal Mistakes** (docs layout): `_data/navigation.yml` holds the sidebar (a key such as `docs:` listing `title` + `children: [{ title, url }]`), and pages opt in with `sidebar: { nav: \"docs\" }`. Convert it like a config-declared nav (SKILL.md). Notices `{: .notice }` and `.notice--primary`/`--info`/`--warning`/`--success`/`--danger` → pass those names with `--callouts` and map the types by hand; `toc: true` and `toc_*` → delete.\n- **Documentation Theme for Jekyll:** `_data/sidebars/*.yml` (`entries` → `folders` → `folderitems`, then `subfolders` → `subfolderitems`), with `sidebar: <file>` and a flat `permalink: name.html` on every page. `{% include note.html content=\"…\" %}` (also `tip`, `warning`, `important`, and `callout.html` with a `type`) → directives; `summary` → `description`; `keywords` → `search.keywords`; `last_updated` → `lastModified`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts` (a Ruby site has none: scaffold it), Node 22.19+, `.gitignore` for `node_modules/`, `.blume/`, `dist/`, `.env.local`.\n- **CI:** replace `bundle exec jekyll build` and the Pages starter workflow with the Blume one (https://useblume.dev/guides/markdown-docs-github-pages). Its `npm ci` needs a lockfile, so commit the `package-lock.json` from `npm install` in the same change. A branch-deployed Pages site moves its source to GitHub Actions in the repo settings, where the custom domain also stays: list both as manual steps. Dependabot's `bundler` entry → `npm`. Scripts that wrapped Jekyll (`serve.bat`, `Rakefile`) → delete or repoint.\n- **Delete** `_config.yml`, `Gemfile`, `Gemfile.lock`, `_layouts/`, `_sass/`, theme hook and HTML includes, JTD's `assets/` files, `_site/`, `.jekyll-cache/`, and `vendor/bundle/`, once everything they declared is harvested. Keep `_includes/` partials pages still `<include>`, and `jekyll-migration.json`.\n- **Grep the whole repo** for moved page paths and for `jekyll` (CODEOWNERS, link checkers, a README \"build the docs\" section).\n\n## Leftover check\n\nRerun until it prints only code samples, `{{name}}` variables and include props you defined, and relative links you meant:\n\n```bash\ngrep -rnE '\\{:[ .#a-z:]|\\{%|\\{\\{|^\\*\\[|^: |markdown=\"|<i class=\"fa|\\.html[)#\"]|\\]\\(\\.\\.?/[^).]*\\)|\\]\\(/[^)]*/(#[^)]*)?\\)' \\\n  --include='*.md' --include='*.mdx' --exclude-dir={node_modules,vendor,_site,.blume,dist} .\n```\n\n## Dropped — report these\n\nCallout title labels that only named the type, unconfigured or malformed callout IALs left plain, the button and label styling the codemod dropped, `nav_external_links` moved above the sidebar, `back_to_top`, other `footer_content` text and links inside the copyright, theme hooks, custom color schemes beyond the accent, `_sass` rules with no Blume target, content a CSS rule hid, site-wide code line numbers, deeper TOC levels, `UA-` analytics ids, `sitemap: false`, abbreviations, definition-list styling, Liquid logic and `_data` loops now frozen as content, embed widths, page scripts and widgets (sliders, accordions) not rebuilt, icon-font icons with no Lucide match, accidentally published files (`LICENSE`, `.bat`), and every plugin removed without an equivalent.\n","isInternal":false,"tokens":8919,"sizeBytes":32775},{"name":"mdbook.md","path":".agents/skills/blume-migrate/references/mdbook.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/mdbook.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# mdBook → Blume\n\nmdBook is the Rust project's book generator: a `book.toml`, a hand-written `SUMMARY.md` that is the whole table of contents, and chapters in `src/` rendered by pulldown-cmark after preprocessors rewrite them (the built-in `links` expands `{{#include}}`; plugins like mdbook-admonish and mdbook-mermaid add the rest). Watch for five traps:\n\n- **Every URL ends in `.html`** (`src/guide/setup.md` → `/guide/setup.html`, a `README.md` → `index.html`), so every chapter that isn't an index needs an exact redirect.\n- **`SUMMARY.md` is the only navigation, and its nesting rarely exists on disk.** A parent chapter is a page _and_ a group, and its file sits beside its children's folder (`guide.md` + `guide/`) or flat next to them.\n- **`{{#include}}` pulls code from anywhere in the repo, by `ANCHOR` name or line range.** Blume's `<include>` takes whole files from inside the content root only, so code the crate compiles goes through generated excerpts (Includes). Anchors that no longer exist rendered as empty blocks on the old site, with no warning, so expect several.\n- **Fences carry Rust-only syntax.** Hidden `# ` lines and attributes like `rust,ignore` show up as code or kill highlighting if left alone.\n- **The page heading is the body H1, but the sidebar label and `<title>` are the `SUMMARY.md` text**, and the two often differ.\n\nScope: mdBook 0.4.x and 0.5.x, checked against 0.4.48, 0.4.52, and 0.5.4, mdbook-admonish 1.19.0, mdbook-mermaid 0.15.0, and an end-to-end migration of the Linera manual (39 chapters, mdBook 0.4.48). The syntax of mdbook-katex, -toc, -tabs, and -alerts comes from their docs; it wasn't run.\n\n## Detect\n\n- **`book.toml`**, with `[book]` and usually `[output.html]` tables: only mdBook uses it. It sits at a standalone book's root or in a folder of a code repo (`docs/`, `book/`, `doc/`, `guide/`, `mdbook/`). The chapters live in `[book] src` (default `src/`), which can point outside that folder (`src = \"../docs\"`).\n- **`SUMMARY.md`** in that `src`. GitBook and HonKit use `SUMMARY.md` too, but with `.gitbook.yaml` or `book.json`, never `book.toml`.\n- CI that runs `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, or `cargo install mdbook mdbook-<plugin>`. The workflow that deploys may live in another repo (one that checks the code repo out as a submodule).\n- **Fingerprint the version** from the CI pin. 0.5 removed `curly-quotes`, `multilingual`, `google-analytics`, and `copy-fonts`, rejects unknown `book.toml` keys, and fails on plugins built for 0.4 (mdbook-admonish 1.19.0 and mdbook-mermaid 0.15.0 both do), so most books with plugins still build with 0.4.x.\n\n**Where the Blume project goes.** Beside `book.toml`, the folder `mdbook build` ran in, with `content.root` set to the book's `src` (Blume's default is `docs`, so set it unless `src = \"docs\"`). That keeps `package.json`, `public/`, and `dist/` out of a Rust repo's root. mdBook has no manifest: scaffold `package.json` there (SKILL.md step 6). **A `src` outside that folder (`\"../docs\"`) works**, edit links included, and a `meta.ts` there imports `defineMeta` from `blume` as usual. That import needs a Blume release after 2.1.3: earlier ones resolved `blume` only from the meta file's own folder up, so it failed with `BLUME_META_LOAD_FAILED … Cannot find module 'blume'`. On those, export a plain object instead (`export default { title: \"Developers\", display: \"flat\", pages: […] };`).\n\n## Build the old book first\n\nBefore changing anything, build the book with the version CI pins and its plugins, into a folder outside the repo. Prebuilt binaries of mdBook and most plugins are on their GitHub releases pages (mdbook-admonish has none for Apple Silicon: `cargo install --root <scratch> mdbook-admonish@<version>`); install them in a scratch folder, not globally.\n\n```bash\nmdbook build <book-root> -d /abs/path/old-book\nMDBOOK_OUTPUT='{\"markdown\": {}}' mdbook build <book-root> -d /abs/path/old-md\n(cd /abs/path/old-book && find . -name '*.html' ! -name print.html ! -name toc.html ! -name 404.html | sed 's|^\\.||' | sort) > old-urls.txt\n```\n\n- `old-book` is the old site: its `.html` files are the old URLs, its headings carry the old ids, and its pages show what readers saw. With two or more `[output.*]` tables (`[output.linkcheck]` counts), the HTML lands in `old-book/html/`: use that folder wherever this file says `old-book`.\n- `old-md` is each chapter as Markdown after `{{#include}}` and the other helpers ran (admonish blocks stay as written): what every include actually showed.\n- Under 0.4 a missing plugin only warns, and its blocks render as code; the URLs are still right. Under 0.5 it fails unless `optional = true`.\n\n## Config: `book.toml` → `blume.config.ts`\n\nMap only what's set. CI can override keys with `MDBOOK_BOOK__…`/`MDBOOK_OUTPUT__…` environment variables, so read those too.\n\n| `book.toml` | Blume |\n| --- | --- |\n| `[book] title` | `title`. Unset, mdBook showed no title: take the first chapter's H1, or ask |\n| `description` | `description` |\n| `src` | `content.root` |\n| `language` (not `en`) | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`); `text-direction = \"rtl\"` → that locale's `dir: \"rtl\"` |\n| `authors`, `multilingual`, `[rust] edition`, `[build] build-dir`, `create-missing`, `extra-watch-dirs` | drop. With `create-missing` (on by default) mdBook created an empty page for a `SUMMARY.md` entry whose file was missing: check the old build |\n| `use-default-preprocessors = false` | without `[preprocessor.links]`/`[preprocessor.index]`, `{{#…}}` stayed literal and `README.md` stayed `README.html`: read the old build |\n| `[output.html] site-url = \"/repo/\"` | `deployment: { base: \"/repo\" }`. It's a path, not an origin |\n| `cname` | `deployment: { site: \"https://<cname>\" }`; GitHub Pages isn't auto-detected. A project site with no domain → `{ site: \"https://<owner>.github.io\", base: \"/<repo>\" }` |\n| `git-repository-url` on GitHub | `github: { owner, repo }`. Elsewhere → `navigation.actions` (report) |\n| `edit-url-template` | `github.branch` from the segment after `edit/` or `blob/`. Set `github.dir` to the Blume project's path in the repo (`git rev-parse --show-prefix`), not the template's prefix: mdBook fills `{path}` with `<src>/<file>`, so a `src` outside the book root gave the old site broken edit links (`…/edit/main/../docs/x.md`) |\n| `default-theme`, `preferred-dark-theme` | mdBook switches between them on the reader's system preference. Both dark (`coal`, `navy`, `ayu`) → `theme.mode: \"dark\"`; a light one plus a dark one → omit. A theme switcher deleted from `theme/index.hbs`, or a stylesheet that repaints the default theme, forces one look: read the CSS, not the name |\n| `additional-css` | a root `theme.css` with only the rules whose markup still exists (`.content`, `.sidebar`, `.chapter`, `--links`, `--sidebar-*` match nothing). Read brand colors and fonts into `theme.accent`, `theme.background`, and `theme.fonts`; Blume already defaults to Inter and IBM Plex Mono. Delete `mdbook-admonish.css` |\n| `additional-js` | delete the mermaid, admonish, and KaTeX assets. A gtag loader → `googleAnalytics({ id })` from `blume/analytics`; other scripts → `script()` or report |\n| a `theme/` folder | diff it against `mdbook init --theme` from the same version first: stock files carry nothing to port, and the stock `favicon.png`/`favicon.svg` are mdBook's own logo (report, don't copy). Custom ones: `<head>` meta tags → `seo.metatags`; analytics → adapters; a favicon → `public/`; fonts → `theme.fonts`; the rest drops (report) |\n| `google-analytics` (0.4) | `googleAnalytics({ id })` for a `G-` id; report a `UA-` id |\n| `mathjax-support` | Math (Other syntax) |\n| `smart-punctuation` / `curly-quotes` | Blume always converts quotes and dashes. Where the old site had it off (0.4's default), `--flag` in prose becomes an en dash: put flags in backticks |\n| `no-section-label` | nothing. Without it, mdBook numbered the chapters (`1.`, `2.3.`) in the sidebar, and Blume can't: report it, and fix prose that cites \"chapter 4\" |\n| `input-404`, `src/404.md` | delete the page; Blume ships a 404 (`pages/404.astro` to customize) |\n| `[output.html.search]` | built in: drop. `search.chapter.\"<path>\" = { enable = false }` → `search: { exclude: true }` on those pages |\n| `[output.html.fold]` | Navigation |\n| `[output.html.redirect]` | URLs and redirects |\n| `[output.html.playground]`, `[output.html.code.hidelines]` | Code blocks |\n| `[output.html.print]`, `hash-files`, `copy-fonts`, `sidebar-header-nav`, `git-repository-icon`, `definition-lists`, `admonitions`, `live-reload-endpoint`, `[output.markdown]` | drop |\n| `[output.linkcheck]`, `[output.linkcheck2]` | `blume validate --strict` (`--external` for web links) |\n| other backends (EPUB, PDF) | `export: { pdf: true }` prints one page at a time; a whole-book PDF or EPUB has no equivalent (report) |\n\n## Preprocessors\n\n| `[preprocessor.X]` | Blume |\n| --- | --- |\n| `links` (built in) | Includes |\n| `index` (built in) | rename every `README.md` chapter to `index.md` |\n| `admonish` | Admonitions |\n| `mermaid` | ` ```mermaid ` works as written in `.mdx`; delete `mermaid.min.js` and `mermaid-init.js` (theme settings in `mermaid-init.js` drop) |\n| `katex` | `$…$` → `$$…$$` inside its sentence; `$$` blocks stay; `\\$` → `$`. Custom delimiters → convert from those. A `macros` file has no equivalent: expand the macros |\n| `toc` | delete the `<!-- toc -->` marker (or the configured `marker`): the outline rail replaces it |\n| `pagetoc`, `open-on-gh` | built in (outline rail; `github` edit links): delete |\n| `alerts` | GitHub alerts (Admonitions) |\n| `tabs` | `{{#tabs}}` / `{{#tab name=\"X\"}}` … `{{#endtab}}` / `{{#endtabs}}` → `<Tabs>` / `<Tab title=\"X\">`. `global=\"…\"` drops: Blume syncs same-titled tabs page-wide |\n| `i18n-helpers` (gettext, `po/*.po`) | i18n (Repo integration) |\n| `cmdrun` (`<!-- cmdrun … -->`), `template`, custom ones (`command = \"cargo run …\"`) | diff `old-md/` against `src/` to see what each one wrote, then freeze that output or move the generator into a prebuild script (report) |\n| `linkcheck` | `blume validate --strict` |\n| other diagram plugins (graphviz, plantuml, svgbob) | render each diagram to an SVG once and commit it, or redraw it in mermaid (report) |\n| anything else | read what it does; report it |\n\n## Navigation: `SUMMARY.md` → folders and `meta.ts`\n\n### How mdBook reads `SUMMARY.md`\n\n- A `# Summary` first line is ignored. **Prefix chapters** are bare `[Title](file.md)` lines before the first list, and can't nest. **Part titles** are `# Title` headings (h1 only) over the numbered chapters that follow: plain headings, not links. **Numbered chapters** are list items (`-`, `*`, or `1.`) that nest by indentation. **Suffix chapters** are bare links after the last list. A **draft** is `[Title]()` (a greyed-out entry, no page). A **separator** is a `---` line; it can also split one part's list, leaving chapters after it under no visible heading.\n- Only listed chapters render. Any other `.md` under `src/` isn't published, but every non-Markdown file there is copied to the site at its own path.\n- A chapter's URL is its path with `.html`; a `README.md` (any case) becomes `index.html`. **The first chapter is also written to `/index.html`.** Its sidebar label and `<title>` are the `SUMMARY.md` text (plus ` - <book title>`); the page shows its own H1.\n\n### Put every chapter in place\n\nKeep each chapter's path, so its route is the old URL minus `.html`, and keep a table of old URL → file → route as you go: it becomes `redirects`.\n\n- **Every `README.md` chapter → `index.md`.** Its route is the folder's, which `x/index.html` already served. Left as is, Blume publishes it at `/x/README`.\n- **A parent chapter beside its children's folder** (`guide.md` + `guide/`): `git mv guide.md guide/index.md`. The route stays `/guide` and the folder row links to it; left in place, `guide.md` is a separate page beside an unlinked \"Guide\" group. The page moved a level deeper: rewrite its relative links, images, and include paths, and every link to `guide.md`.\n- **Children flat beside their parent** (`ch03-00-concepts.md`, `ch03-01-variables.md`): a `(group)/` folder holding the parent as `index.md` with `slug: ch03-00-concepts`, plus the children. The group adds no URL segment, the `slug` keeps the parent's route, and the group row links to it. Rebase relative paths in every moved file.\n- **The first chapter**, when it isn't the root `README.md`: move it to the content root's `index.md` and redirect its own URL to `/` (`{ from: \"/introduction.html\", to: \"/\" }`), since mdBook served it at both.\n- **Ordering prefixes.** Blume drops a leading `\\d+[-_.]` from every segment, keeping versions (`1.2.0`) and dates (`2024-01-05`, `12-05-2022`, `2024-01`) whole (`00-overview.md` → `/overview`). Every old URL is redirected anyway, so let Blume drop them (they keep ordering the sidebar) and point those redirects at the new routes, or pin `slug` on each page to keep the old path. Say which.\n- **Files `SUMMARY.md` doesn't list** were never published: a contributors' `README.md` beside the chapters, notes, Markdown that `{{#include}}` pulls in. Prefix them with `_`, move them out of the content root, or add them to `content.exclude` (it adds to the defaults `\"**/_*\"` and `\"**/.*\"`). Report them.\n- **A chapter whose body is a `<meta http-equiv=\"refresh\" content=\"0; url=…\">`** is a redirect, not a page to keep: add a redirect from its `.html` URL to the target, a `navigation.featured` link in place of its sidebar entry, and delete it (mdBook's `print.html` ran the same tag, so it sent the whole print page away too). Report it.\n- Delete `SUMMARY.md` once folders and `meta.ts` carry its structure: it would publish as a page.\n- **Check that git sees the new files.** A repo `.gitignore` can cover the content root (pages added before the rule stay tracked), and then every `meta.ts` and partial you create is silently left out of `git add`. Run `git check-ignore -v <content root>/meta.ts` after writing the first one; if it prints a rule, fix that rule or report that each new file needs `git add -f`.\n\n### Parts, order, labels, and folding\n\n- **Display:** set `navigation: { sidebar: { display: \"group\" } }`. Parent chapters become collapsible rows, like mdBook's tree, and a group keeps leaf chapters between its subgroups in `meta.ts` order (SKILL.md: `flat` can't interleave them).\n- **Part titles** → a folder per part, with `title: \"<part title>\"` and `display: \"flat\"` in its `meta.ts`: a heading, not a row. When a part's chapters already share a folder (`developers/`), that folder is the part; otherwise use a `(part)/` folder at the content root, which changes no URL. Chapters after a separator inside a part get the folder they're already in (`appendix/` reads \"Appendix\"), or the part's heading.\n- **Order** → a `meta.ts` `pages` array in each folder, every child in `SUMMARY.md` order. The root `meta.ts` orders the parts.\n- **Prefix chapters** stay loose pages at the content root, which Blume lists first, as mdBook did.\n- **What files can't express:** at the top of the sidebar, loose pages always list above folders. Suffix chapters, and leaf chapters between parent chapters in a book with no parts, rise above them. Put each in a `(group)/` folder (it adds a heading), or accept the order. When many chapters are affected, an explicit `navigation.sidebar` keeps `SUMMARY.md`'s order exactly: a route string per leaf chapter, `{ label, root: \"<route>\", items }` per parent chapter, `{ label, display: \"flat\", items }` per part. It replaces the generated tree, so it needs an entry per new page, as `SUMMARY.md` did. Report which you chose.\n- **Labels:** `title` is the body H1, what the page showed. `sidebar.label` is the `SUMMARY.md` text where it differs, what the sidebar and `<title>` showed. A page with no H1 takes the `SUMMARY.md` text as `title`. Give each parent chapter's folder `title: \"<its SUMMARY.md text>\"` in `meta.ts`, or its row shows the humanized folder name. Strip Markdown from `SUMMARY.md` text: backticks render literally in `title`.\n- A parent's `index` page also lists as its group's first row, so the label shows twice. To show it once, as mdBook did, set `sidebar.hidden: true` on the index: it stays in `sitemap.xml` and search because the group row still links to it. If its `title` differs from the folder's `meta.ts` title, set its `sidebar.label` to the folder title, or `BLUME_NAV_INDEX_TITLE_MISMATCH` fails `validate --strict`.\n- **`[output.html.fold]`:** with `enable = true`, sections below `level` started closed (0 closes all), which is `group` mode's default; add `collapsed: false` to the folders mdBook opened. Without folding, mdBook showed every section open: add `collapsed: false` to every folder's `meta.ts`.\n- **Drafts and separators** have no equivalent: drop them, and report the drafts as planned chapters.\n\n## Content\n\n### `.md` or `.mdx`\n\nRename a page to `.mdx` when it ends up with a callout, mermaid, math, tabs, or another component. Those are MDX-only: in `.md` they don't render, and the build stays green (a `:::` callout only warns `BLUME_MD_DIRECTIVE`). Tables, footnotes, strikethrough, task lists, `[#id]` heading ids, `<include>`, `{{variables}}`, and raw HTML work in both (`{#id}` only in `.md`). Fix what fails `.mdx` in each renamed page: HTML comments → `{/* … */}` on one line, unclosed `<br>`/`<img>`, `<https://…>` autolinks, and `{`, `<` in prose (`Vec<T>` outside backticks).\n\n### Includes and the other `{{#…}}` helpers\n\nmdBook expands these as text before parsing, so they work inside fences, inline code, and link URLs, and across a line break (`{{#include` at a line's end, the path on the next, as Prettier wraps it). A path resolves from the including file.\n\n| mdBook | Blume |\n| --- | --- |\n| a fence holding only `{{#include path}}`, the whole file, inside the content root | the whole fence → `<include lang=\"rust\">path</include>` on its own line (the fence's language; its attributes drop) |\n| a fence holding only `{{#include path:anchor}}`, `path:N`, `path:N:M`, `path:N:`, or `path::M`, or any target outside the content root | an excerpt (below), included the same way |\n| a fence mixing includes with hand-written lines | one excerpt that joins `{ \"text\": \"…\" }` parts and selections in order, so the block stays generated |\n| `{{#rustdoc_include path:sel}}` | an excerpt of `path:sel`: the selected lines only. The rest of the file sat behind mdBook's expand toggle, which has no equivalent (report) |\n| `{{#playground path attrs}}` | `<include lang=\"rust\">` of the file (an excerpt when it's outside the root); no Run button (report) |\n| `{{#include x.md}}` on its own line | `<include>/_snippets/x.md</include>` (a leading `/` is the content root): a partial must sit under a `_` folder or name, or it publishes as a page. Images in a partial resolve from the partial's own folder (mdBook resolved them from the including chapter), so fix their paths |\n| a one-line file included inline (`{{#include ../VERSION}}` in a URL or a command) | `{{VERSION}}`, with `variables: { VERSION: readFileSync(new URL(\"../VERSION\", import.meta.url), \"utf8\").trim() }` in `blume.config.ts` (`import { readFileSync } from \"node:fs\"`). Variables are replaced in links, code, and inline code too |\n| `{{#title X}}` | `seo.title: X` (Blume appends the site title, as for every page) |\n| `\\{{#include x}}` (often in a fence that documents mdBook) | drop the backslash, which mdBook removed even inside code. `{{#include x}}` stays literal in code and in `.md`; in `.mdx` prose a `{` fails the build, so put it in inline code |\n\n**Excerpts.** `<include>` fails the build on a target outside the content root (`BLUME_INCLUDE_OUTSIDE_ROOT`) and can't select lines. Pasting each excerpt into its page would freeze code that cargo still compiles and tests, so generate them before every run with the bundled `include-excerpts.mjs`:\n\n1. Copy `<skill>/scripts/include-excerpts.mjs` beside `blume.config.ts`. Its header documents the manifest.\n2. Write `excerpts.json` there: `out` (`<content root>/_excerpts`, relative to the manifest; Blume doesn't publish a `_` folder), and one entry per excerpt, keyed by its file name under `out`. An include's argument works as written once its path is relative to the manifest's folder (`\"counter/abi.rs\": \"../examples/counter/src/lib.rs:contract_abi\"`): the script selects anchors and line ranges exactly as mdBook did. A mixed fence is an array: `[{ \"text\": \"impl Contract for Token {\" }, \"../src/contract.rs:transfer\", { \"text\": \"}\" }]`.\n3. Run `node include-excerpts.mjs`. It checks every entry first and lists **every** missing file, missing anchor, and line range past the end of its file in one pass, then writes nothing. A clean run writes the excerpts and deletes the ones it wrote earlier that the manifest dropped (never a file it didn't write).\n4. Replace each include with `<include lang=\"rust\">/_excerpts/counter/abi.rs</include>`.\n5. Run it first in `package.json` (`\"dev\": \"node include-excerpts.mjs && blume dev\"`, and the same for `build` and a `validate` script), and gitignore `<content root>/_excerpts/`. A host's build command must be `npm run build`, not `blume build`. Rerun it after editing a source file: `blume dev` doesn't watch them.\n\n**Missing anchors.** For each anchor the script reports (the old site showed an empty block, or only a mixed block's hand-written lines), find where the code went (`git log -S \"ANCHOR: <name>\"`, or search the crate for the item), then:\n\n- **When the team owns the source, re-add the `// ANCHOR: <name>` and `// ANCHOR_END: <name>` comments around the code there.** The entry keeps working as written, and the excerpt follows the code as it changes.\n- Otherwise, point the entry at a line range (`\"…/lib.rs:14:23\"`). It keeps selecting those line numbers when the file changes, without an error, so say so in the report.\n- Drop the block, with its lead-in sentence, when the code is gone.\n\nReport each one and what you chose. Code that now fills a formerly empty block may contradict the prose around it: list what you notice.\n\n### Code blocks\n\n| mdBook | Blume |\n| --- | --- |\n| ` ```rust,ignore `, `rust,no_run,noplayground`, `rust,should_panic`, `rust,compile_fail`, `rust,edition2021`, `rust,editable`, `rust,mdbook-runnable`, `rust,hidelines=…` | ` ```rust ` (a comma-glued language doesn't highlight, and warns `BLUME_UNKNOWN_CODE_LANGUAGE`; written with a space, ` ```rust ignore `, the keyword does nothing and warns `BLUME_CODE_FENCE_OPTION`) |\n| ` ```ignore `, or no language: rustdoc tests an unlabeled block as Rust, so books label diagrams and shell lines `ignore` | ` ```text `, or the real language |\n| **hidden lines** in a Rust block: first non-space character `#`, then a space or the end of the line | delete the line, and a blank line it leaves at the top of the block: readers saw it only through the eye toggle, which has no equivalent. `##x` shows as `#x`; `#[derive]` and `#!` lines are code. Lines with a `[output.html.code.hidelines]` prefix (`python = \"~\"`), or a fence's `hidelines=<prefix>`, go the same way |\n| the play button, `editable` | static code (report) |\n| `mdbook test` | nothing compiles Blume's code blocks. Excerpts stay tested by cargo; hand-written blocks that `mdbook test` compiled aren't any more (report; drop the CI step and whatever it built only for that) |\n\n### Admonitions\n\n**mdbook-admonish** writes ` ```admonish <type> ` with options (`title=\"…\"`, `collapsible=true`, `id`, `class`), the older `admonish <type> \"Title\"`, or `~~~`/four-backtick outer fences around inner code. Each becomes a callout in `.mdx`, with a longer `::::` outer fence around a nested one:\n\n| admonish type (aliases) | Blume |\n| --- | --- |\n| `note`, `example` | `:::note` |\n| `info` (`todo`), `abstract` (`summary`, `tldr`), `question` (`help`, `faq`) | `:::info` |\n| `tip` (`hint`, `important`) | `:::tip` |\n| `success` (`check`, `done`) | `:::success` |\n| `warning` (`caution`, `attention`) | `:::warning` |\n| `danger` (`error`), `failure` (`fail`, `missing`), `bug` | `:::danger` |\n| `quote` (`cite`) | a blockquote |\n| **any other word** (`admonish warn`) | `:::note`: admonish rendered it as a note titled \"Note\". Don't keep the word: Blume reads `:::warn` as a warning. Report it, with the type the author probably meant |\n\n- `title=\"T\"` → `:::type[T]`; `title=\"\"` → no title. An untitled block showed the word it was written with as its title bar (`Example`, `Hint`, `TL;DR`), and an untitled Blume callout shows none: where that word isn't the Blume type's name, keep it as the title (`:::note[Example]`).\n- `collapsible=true` → `<Expandable title=\"T\">` (the type's name when untitled); its color drops.\n- Custom directives (`[[preprocessor.admonish.custom]]`) → the closest callout, with the label as title.\n- Each block had an anchor (`#admonition-<title>`, `-1` for repeats). Links to one fail `blume validate --strict`: point them at the nearest heading or drop the fragment.\n\n**GitHub alerts** (`> [!NOTE]`, built into 0.5 and the `alerts` plugin) render as callouts in `.mdx` as written: `NOTE` and `IMPORTANT` as a note, `TIP` a tip, `WARNING` a warning, `CAUTION` a danger callout. In `.md` they stay a blockquote with a literal `[!NOTE]` and warn `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. A 0.4 book without the plugin showed them as plain quotes: rendering them changes the page (report).\n\n### Other syntax\n\n| mdBook | Blume |\n| --- | --- |\n| `## Heading {#id}` | the same in `.md`; `[#id]` in `.mdx` |\n| `## Heading { #id .class }` | `{#id}` or `[#id]`; classes drop. Left as is, the spaced form stays in the heading text and makes a garbage id; `BLUME_MD_CURLY_ANCHOR` flags it only when no class follows the id (in `.mdx` it fails the build) |\n| footnotes, tables, strikethrough, task lists | unchanged |\n| definition lists (`term` then `: definition`, on by default in 0.5) | a list or table, or raw `<dl>`; Blume renders them as one paragraph |\n| MathJax (`mathjax-support`): `\\\\( … \\\\)`, `\\\\[ … \\\\]` | `$$…$$` inside the sentence, or on lines of its own for a block, in `.mdx`. Undo the Markdown escapes mdBook needed inside the math (`\\\\\\\\` for a LaTeX `\\\\`, any `\\_` or `\\*`): `$$` math is read verbatim |\n| `<i class=\"fa fa-x\"></i>` | `<Icon icon=\"<lucide>\" />` in `.mdx`, or delete (the icon map in `references/mintlify.md`) |\n| `class=\"left\"`/`\"right\"` floats; `class=\"hidden\"` | drop the class; delete hidden elements (report) |\n\n### Links and images\n\n- mdBook only rewrote `x.md` links to `x.html`. Blume resolves a relative `.md` link to the page that file publishes: keep them, and fix the ones into or out of every moved file.\n- Rewrite `.html` links (`installation.html#cargo`) to their `.md` files: Blume serves no `.html` URLs, so they'd depend on the redirects. `blume validate` reports each one left (`BLUME_BROKEN_ASSET`, with the page's route).\n- **A link to `README.md` was already dead:** mdBook wrote `README.html`, a page it never creates. Point it at the renamed `index.md`.\n- Root-relative links (`/guide/x.md`) land on the page that file publishes, read from the content root, like relative ones.\n- Images relative to the page work as written. So does a linked download (a PDF, an archive) that mdBook copied from `src/`: Blume publishes a file a relative link names with the page, at a new URL. Move it to `public/` at the same path only if its old URL is linked from elsewhere.\n\n## URLs and redirects\n\n| Source file | mdBook URL | Blume route |\n| --- | --- | --- |\n| `src/guide/setup.md` | `/guide/setup.html` | `/guide/setup` |\n| `src/guide/README.md` | `/guide/index.html` | `/guide` |\n| the first chapter, `src/intro.md` | `/intro.html` and `/index.html` | `/` once moved to `index.md` |\n| `src/01-basics.md` | `/01-basics.html` | `/basics`, or `slug: 01-basics` |\n\n- **One exact redirect per chapter that isn't an index**, `{ from: \"/guide/setup.html\", to: \"/guide/setup\" }`, built from `old-urls.txt` and your table: a pattern can't strip `.html`. Skip `/index.html` and `/x/index.html`: static hosts serve Blume's `x/index.html` there already.\n- **`[output.html.redirect]`**: each key is a path from the site root, and its value is a URL relative to the key's folder (`\"../infra/x.html\"`), root-relative, or external. Resolve the value against the key's folder, then map it like a chapter (`.html` → route, `index.html` → its folder); external URLs stay. Check each target against the old build: one that already led to a 404 → the page that replaced it, or drop it, and report.\n- **Fragment redirects** (`\"/page.html#old\" = \"other.html#new\"`, 0.4.52 and later): servers never see fragments. A heading renamed on the same page → pin the old id on it (`[#old]`); moved to another page → no server equivalent (report). A deleted page's own entry is its redirect.\n- **No equivalent, no redirect:** `print.html` (the whole book on one page, `noindex`), `toc.html` (the sidebar frame, `noindex`), and `404.html` (Blume writes its own). Mention `export: { pdf: true }` for printing.\n- **Hosts:** a static build writes each redirect as a meta-refresh page at `dist/<from>/index.html`, plus `_redirects` and `vercel.json`. On Netlify use `netlify({ output: \"static\" })` for real 301s (SKILL.md). GitHub Pages reads neither file and serves the meta-refresh page; how it answers `/x.html` there is unverified, so check a few old URLs after deploying.\n- **Heading anchors:** mdBook 0.5 ids match Blume's for most headings; 0.4 lowercases ASCII only (`## Über` → `#Über`), and both count the H1, so `# Install` then `## Install` gives `#install-1`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old old-book` (add `--map dist/blume-redirects.json` when routes changed, built with no host adapter; `--write` to apply), and rebuild until it reports 0.\n- **Check every old URL** after `blume build`. With no host adapter (static), from the Blume project:\n\n  ```bash\n  node -e '\n  const fs = require(\"node:fs\");\n  const moved = new Map(JSON.parse(fs.readFileSync(\"dist/blume-redirects.json\", \"utf8\")).map((r) => [r.from, r.to]));\n  const built = (to) => /^https?:/.test(to) || fs.existsSync(`dist${to.replace(/#.*/, \"\").replace(/\\/$/, \"\")}/index.html`);\n  const misses = fs.readFileSync(\"old-urls.txt\", \"utf8\").split(\"\\n\").filter(Boolean).filter((url) => {\n    const to = url.endsWith(\"/index.html\") ? url.slice(0, -\"index.html\".length) : moved.get(url);\n    return !(to && built(to));\n  });\n  console.log(misses.length ? misses.join(\"\\n\") : \"every old URL resolves\");'\n  ```\n\n  Then run `blume audit --only redirects`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts`: `blume` plus the `dev`/`build`/`preview`/`validate` scripts, with the excerpt step first. Commit the lockfile (SKILL.md step 6).\n- **`.gitignore`:** replace `book/` with `dist/`, `.blume/`, `node_modules/`, `.env.local`, and `<content root>/_excerpts/`.\n- **CI:** replace `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, and the plugin installs with Node 22, an install, `npm run build`, and `npm run validate`. A deploy workflow in another repo needs the same change: list it as a follow-up. For GitHub Pages, follow https://useblume.dev/guides/markdown-docs-github-pages (Actions as the Pages source), with `npm run validate` and `npm run build` in place of its `blume` steps so the excerpts exist, run from the Blume project's folder, and that folder's `dist` uploaded. A deploy that pushes a branch must publish `.nojekyll`, or Jekyll drops Blume's `_astro/` folder; `peaceiris/actions-gh-pages` adds it by default.\n- **`cargo doc` output published beside the book** (`/api/…`): keep building it in CI and copy `target/doc` into the build output, or link to docs.rs. Blume doesn't generate it.\n- **Several builds of the book** (a stable tag and `main`, `MDBOOK_BOOK__LANGUAGE` per language): migrate the current one; older versions → `versions`, or stay on the old host (report). Translations in `po/*.po` (mdbook-i18n-helpers) apply at build time: render each language's Markdown with `MDBOOK_BOOK__LANGUAGE=<xx>` and the Markdown renderer into `<xx>/`, then set `i18n`. Report it.\n- **Prettier** on the book: extend its `*.md` overrides to `*.mdx`, and keep `{/* */}` comments on one line. Prettier 3 skips what `.gitignore` ignores, so a `.gitignore` that covers the content root also makes a `prettier --check` CI step check nothing.\n- **A hand-written `llms.txt` in `src/`:** Blume generates `/llms.txt`, so delete the file. Its intro goes to `agents.llmsTxt.details`. A per-page summary becomes that page's `description` only where it reads well as the page's lede, since Blume shows `description` under the title; report the rest, and the sections that list things other than pages.\n- **Delete** `book.toml`, `SUMMARY.md`, `theme/`, the `additional-css`/`additional-js` files once ported, `mdbook-admonish.css`, `mermaid.min.js`, `mermaid-init.js`, and the `book/` folder. Grep the repo for `mdbook`, `book/`, and the old `.html` URLs (READMEs, CONTRIBUTING, examples, bots).\n\n## Dropped — report these\n\nSection numbers, separators, and draft chapters; suffix chapters and leaf chapters that moved above folders; the print page; the theme switcher and themes beyond one light and one dark; search options; the play button, the editor, and runnable or editable examples; hidden lines and their toggle; `mdbook test` coverage of hand-written blocks; `rustdoc_include` context; admonish title bars, colors, icons, custom directives, and anchors; fragment redirects across pages; the old site's empty blocks from missing anchors, and what you did with each; definition-list styling; Font Awesome icons with no Lucide match; `class=\"hidden\"` content; plugin output you froze (cmdrun, templates, custom preprocessors); a hand-written `llms.txt`; whole-book PDF or EPUB output; and every plugin you removed without an equivalent.\n","isInternal":false,"tokens":9352,"sizeBytes":34165},{"name":"mintlify.md","path":".agents/skills/blume-migrate/references/mintlify.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/mintlify.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Mintlify → Blume\n\nThe deepest migration. Mintlify declares its **entire navigation in `docs.json`**; Blume derives navigation from the filesystem. The bulk of the work is reshaping content into folders + tabs and translating config, callouts, and icons.\n\n## Detect\n\n- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.\n- A `mintlify` dependency / `mintlify dev` script.\n- Content is `.mdx` under the repo root (Mintlify has no `content.root` — pages live at the top level), with a `/snippets` folder and image dirs like `/images`.\n\nRead `docs.json` first; it drives everything.\n\n## Config: `docs.json`/`mint.json` → `blume.config.ts`\n\nResolve `$ref` includes first (Mintlify splits config across files). Map only what's set:\n\n| Mintlify | Blume | Notes |\n| --- | --- | --- |\n| `name` / `title` | `title` |  |\n| `description` | `description` |  |\n| `logo` (string or `{ light, dark, href }`) | `logo` (string, or `{ image: { light, dark, alt }, text, href }`) | Mintlify's `light`/`dark` nest under Blume's `image`; ensure files land in `public/`. **If the SVG is local and monochrome** (a solid black or white mark), rewrite its `fill`/`stroke` to `currentColor` and collapse `{ light, dark }` to the string shorthand `logo: \"/logo.svg\"` — one file that inherits the theme text color and flips with light/dark automatically. If the logo is a **wordmark** (brand name baked in), set `text: \"\"` so it doesn't render twice beside `title` |\n| `favicon` (string **or `{ light, dark }`**) | **drop the field** — copy the file(s) into `public/` under conventional names (`favicon.svg`/`icon.png`) | Blume auto-detects by filename; there is **no** favicon config field. A `{ light, dark }` source maps to a filename pair: light file → `public/icon.png`, dark file → its `-dark` sibling `public/icon-dark.png` (same directory and extension — convert if the formats differ) |\n| `colors.primary` + `colors.light` | `theme.accent` (`{ light, dark }`) | accent is now **per-mode**: `accent: { light: <colors.primary>, dark: <colors.light> }`. If Mintlify sets only `primary`, collapse to the string shorthand `accent: \"<colors.primary>\"` (applies to both modes) |\n| `colors.dark` | `theme.action` |  |\n| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) |  |\n| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |\n| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |\n| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |\n| `background.decoration` | **drop** | no Blume equivalent |\n| `fonts.family` / `fonts.{heading,body}.family` | `theme.fonts.{display,body}` | a curated slug (kebab-case, e.g. `space-grotesk`) maps directly; any other Google family maps to the object form `{ name: \"Family Name\" }`; a self-hosted font (`fonts.*.src` URLs) maps to `{ name, variants: [{ src, weight }] }` after downloading the files into the project |\n| `banner` | `banner` (`{ content, dismissible, id, link: { href, text } }`) | **only** those keys — drop `banner.color`/`banner.type` |\n| `styling.latex: true` | **drop the field** — block math `$$…$$` renders in `.mdx` with no config | there is **no** `markdown.math` field; Blume writes inline math with two dollar signs too — convert each inline `$…$` to `$$…$$` inside the sentence, checking every pair is math and not currency or a shell variable |\n| `styling.codeblocks.theme` | `markdown.code.theme` (`{ light, dark }`) |  |\n| `search.prompt` | **drop** | no equivalent |\n| `seo.metatags` | `seo.metatags`, minus the tags Blume writes itself | keep verification tokens, `theme-color`, and the like as written; Blume refuses `description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, and `article:*` dates, so move those to their settings (`og:image` → `seo.og` or a page's `seo.image`, `twitter:site` → `seo.x.handle`) and drop the rest. `seo.paths` (folder tags) has no equivalent: set per-page `seo` frontmatter |\n| `seo.indexing: \"all\"` | `search.indexing.includeHiddenPages: true` |  |\n| `variables` (`{{name}}`) | `variables` | same syntax and names (letters, digits, `_`, `-`); pages keep their `{{name}}` references unchanged |\n| `integrations.posthog` (`{ apiKey, apiHost, sessionRecording }`) | `posthog({ key, host })` in the `analytics` list | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`); `sessionRecording: false` → `disable_session_recording: true`; every adapter is imported from `blume/analytics` |\n| `integrations.ga4` (`{ measurementId }`) | `googleAnalytics({ id })` |  |\n| `integrations.gtm` (`{ tagId }`) | `googleTagManager({ id })` |  |\n| `integrations.plausible` (`{ domain, server }`) | `plausible({ domain, host })` | `server` is a bare hostname; `host` is an origin — prepend `https://` |\n| `integrations.fathom` (`{ siteId }`) | `fathom({ site })` |  |\n| `integrations.pirsch` (`{ id }`) | `pirsch({ code })` |  |\n| `integrations.mixpanel` (`{ projectToken, region }`) | `mixpanel({ token, region })` |  |\n| `integrations.amplitude` (`{ apiKey }`) | `amplitude({ key })` |  |\n| `integrations.segment` (`{ key, cdnUrl }`) | `segment({ key, cdn })` |  |\n| `integrations.hightouch` (`{ writeKey, apiHost }`) | `hightouch({ key, host })` |  |\n| `integrations.heap` (`{ appId }`) | `heap({ id })` |  |\n| `integrations.hotjar` (`{ hjid, hjsv }`) | `hotjar({ id, version })` | both numbers |\n| `integrations.clarity` (`{ projectId }`) | `clarity({ id })` |  |\n| `integrations.logrocket` (`{ apiKey }`) | `logrocket({ id })` |  |\n| `integrations.clearbit` (`{ publicApiKey }`) | `clearbit({ key })` |  |\n| `integrations.adobe` (`{ launchUrl }`) | `adobe({ url })` |  |\n| any other `integrations` script | `script({ src, strategy, attributes })` / `vercel()` in the `analytics` list | one `script()` adapter per provider without a factory |\n| `telemetry` (`{ enabled: false }`) | `feedback: false` | Mintlify's switch also turned off its feedback widgets; Blume sends no telemetry of its own, so only the rating needs turning off |\n| Feedback toggles in the Mintlify dashboard (not in `docs.json`) | `feedback: { comments: true }` for contextual (written) feedback | ask whether the site collected written feedback; Blume sends comments through the `analytics` adapters as `feedback_comment` events. Code-snippet feedback, edit suggestions, and raise-issue have no equivalent: report them |\n| `integrations.cookies` (`{ key, value }`), or a content-folder `.js` file that injects Osano or Ethyca/Fides | `consent`, imported from `blume/consent`: `osano({ customerId, configId })` (both IDs from the `cmp.osano.com/<customerId>/<configId>/osano.js` URL), `ethyca({ privacyCenter })` (the origin that serves `fides.js`), or `native()` for Blume's own banner | Mintlify's `cookies` pair only switched off its own telemetry for readers whose localStorage lacked the value; under `consent`, every `analytics` adapter waits for the reader instead. Delete the injector script. Another consent manager (Transcend, OneTrust, …) has no adapter: report it |\n| A root `skill.md`, or `.mintlify/skills/<name>/SKILL.md` | move each into a `skills/<name>/` folder and set `agents.skills: \"./skills\"`; drop Mintlify-only frontmatter (`groups`) | Blume already generates a skill for the site at `/skill.md` (`agents.skillMd`, on when `deployment.site` is set); a root `skill.md` replaces it only if its `name` matches the site's title slug (`Acme Docs` → `acme-docs`), so set that name |\n| `contextual` (`[\"copy\",\"chatgpt\",\"claude\",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `agents.mcp.enabled` + server output (report as a follow-up) |\n| `footer` (`{ socials, links }`) | `footer: { socials, links }` | `socials` keeps its keys, except `twitter`/`x-twitter` → `x` and `earth-americas` → `website`; a key outside Blume's list drops (report); `links` is one row of `{ label, href }`: every column's `items` flatten into it in order, and each column's `header` drops (report) |\n| `api.mdx` (`{ server, auth: { method, name } }`) | `api: { server, auth: { method, name } }` | the defaults for `api`-frontmatter pages |\n| `api.playground` (`{ display, proxy }`) | `api: { playground }` | `display: \"none\"` → `playground: false`; `\"simple\"` has no site-wide form, so set `playground: simple` in each page's frontmatter; `proxy` (on by default in Mintlify) → `playground: { proxy: true }`, which needs a server-output adapter, or leave it off to send directly |\n| `redirects` (`{ source, destination, permanent }`) | `redirects: [{ from, to, status }]` | `source` → `from`, `destination` → `to` as written, patterns included (`/beta/:slug*`, `/old/article-*`); `permanent: false` → `status: 307`, otherwise `status: 308` |\n| `navigation.languages` | `i18n` | see i18n below |\n\nA minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.\n\n## Icons: FontAwesome → Lucide (required)\n\nMintlify defaults to **FontAwesome**; Blume is **Lucide-only**. Convert every icon reference — frontmatter `icon`, nav-group `icon`, `<Icon>`, `<Card icon>` — to the closest Lucide name. Discard Mintlify's `iconType` (solid/regular/brands) entirely.\n\n**Automate the frontmatter pass — don't hand-edit it.** `scripts/mintlify-codemod.mjs` (in this skill, zero-dependency) remaps every **frontmatter** `icon:` using the table below, drops icons with no Lucide equivalent, and — in the same pass — drops/renames unsupported frontmatter keys (see [Frontmatter](#frontmatter)). It's deterministic and idempotent, and reports what it changed per file plus what it couldn't (unknown icons, OpenAPI-stub flags):\n\n```bash\n# <skill> = this skill's directory (the one containing SKILL.md); dry run first (report only), then apply:\nnode <skill>/scripts/mintlify-codemod.mjs <content-dir>\nnode <skill>/scripts/mintlify-codemod.mjs --write <content-dir>\n```\n\nThe codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon=\"…\">`, `<Card icon=\"…\">`, nav-group icons in `docs.json`) it does not see — convert those by hand using the same table. Common mappings:\n\n| FontAwesome | Lucide |  | FontAwesome | Lucide |\n| --- | --- | --- | --- | --- |\n| `bolt` | `zap` |  | `gear`/`cog` | `settings` |\n| `circle-info`/`info` | `info` |  | `wand-magic-sparkles`/`magic` | `sparkles` |\n| `house` | `house` |  | `magnifying-glass` | `search` |\n| `gauge`/`gauge-high` | `gauge` |  | `puzzle-piece` | `puzzle` |\n| `envelope` | `mail` |  | `file-lines` | `file-text` |\n| `pen-to-square` | `square-pen` |  | `trash-can` | `trash-2` |\n| `xmark`/`times` | `x` |  | `circle-check` | `circle-check` |\n| `triangle-exclamation` | `triangle-alert` |  | `circle-exclamation` | `circle-alert` |\n| `arrow-right-from-bracket` | `log-out` |  | `right-to-bracket` | `log-in` |\n| `location-dot`/`map-marker` | `map-pin` |  | `comments` | `messages-square` |\n| `cube` | `box` |  | `cubes`/`boxes` | `boxes` |\n| `layer-group` | `layers` |  | `diagram-project`/`sitemap` | `workflow`/`network` |\n| `chart-line` | `chart-line` |  | `chart-simple`/`chart-column` | `chart-column` |\n| `flask` | `flask-conical` |  | `robot` | `bot` |\n| `screwdriver-wrench`/`toolbox` | `wrench` |  | `circle-question`/`question` | `circle-help` |\n| `life-ring` | `life-buoy` |  | `shield-halved` | `shield` |\n| `rocket`/`book`/`book-open`/`code`/`terminal`/`key`/`lock`/`user`/`users`/`database`/`server`/`cloud`/`bell`/`calendar`/`star`/`heart`/`tag`/`folder`/`globe`/`link`/`download`/`upload`/`check`/`copy`/`play`/`filter` | _(same name — verify)_ |\n\n**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*`): Lucide still ships a few brand marks, and Blume renders them under the same name — `facebook`, `github`, `gitlab`, `instagram`, `linkedin`, `slack`, `twitter`, `youtube`. Keep those. The rest (`discord`, `x-twitter`, `docker`, `npm`, `python`, `react`…) have **no** Lucide equivalent, and FontAwesome's `apple` is the Apple logo while Lucide's `apple` is the fruit. Where no Lucide counterpart exists, **drop the icon and report it**. An icon that only marks a link to the repository or a social profile usually has a better home: the `github` config (renders the footer's repo link) or `footer.socials`. The build won't catch a wrong name for you: an unknown icon renders nothing, and it's only a warning (`BLUME_UNKNOWN_ICON`) for navigation icons — an icon prop on a component like `<Card>` fails silently — so check every name.\n\n## Navigation: `docs.json` `navigation` → filesystem + tabs\n\nMintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:\n\n- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. `directory` (`card`/`accordion`/`none`, on a group, tab, or the navigation itself) → `meta.ts` `directory` on that folder, or on the content root's `meta.ts` for the whole navigation; it inherits down the tree the same way, and lists the pages on the group's `root`/`index` page. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display, directory })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)\n- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.\n- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.\n- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `\"GET /path\"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).\n- **`tabs`** (top-level `navigation.tabs`, e.g. `App` / `API` / `API reference`) → Blume's **header tabs** `navigation.tabs` (`{ label, path, icon? }`). **Keep them as tabs — do not flatten them into `navigation.sidebar` groups or sections.** Flattening is the easy translation error: it collapses the site's top-level structure into one global sidebar and loses the header tab bar entirely. A Mintlify top-level tab is a header tab; map it to one. Put each tab's pages in **one folder** and point the tab's `path` at it — the tab then scopes the sidebar automatically (under the tab's `path`, the sidebar shows only that folder). **Blume resolves the active tab by URL prefix** (the tab whose `path` is the longest prefix of the current route), so every page in a tab must live under that tab's single `path`. Mintlify tabs freely mix pages from any folder / arbitrary routes; Blume tabs scope by prefix, so a tab that pulls in routes from all over is **not portable as-is**. You have two choices, per tab:\n  - **Move the pages under one prefix** so they share the tab's `path` (a route change → add a `redirects` entry for each moved page). This preserves the exact tab grouping.\n  - **Accept the closest shape** — assign each shared/cross-cutting page to one tab's folder and link to it from the others — when moving routes isn't worth the churn.\n\n  Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.\n\n- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.\n- **`anchors` / `navigation.global.anchors`** (persistent sidebar-top links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old \"drop and report\" for anchors.)\n- **`navbar.links`** (plain header links — Log in, Status, Support) → **`navigation.actions`** (`[{ label, href }]`), rendered in the header left of the icon buttons. A GitHub link → the `github` config (or `navigation.repo` as a URL when the docs repo is private) instead; it renders in the footer, the only place Blume links a repository. Header links hide on phones; a link that must survive there belongs in `featured`.\n- **`navbar.primary`** (the header **CTA button** — \"Get Started\"/\"Sign Up\", `type: \"button\"`) → **`navigation.cta`** (`{ label, href }`, singular: Blume renders exactly one filled button). A `type: \"github\"` primary → the `github` config instead. A route the docs don't serve (the product's own `/signup`) must be an absolute URL, or it raises `BLUME_NAV_MISSING_PAGE`.\n- **`languages`** → `i18n`, not a selector (see below).\n- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.\n\nThis reshaping **changes URLs** — a page moved from `getting-started/quickstart` into the API tab's folder becomes `/api/…`, an `index` promotion drops a segment, etc. Record each old→new path and add a `redirects` entry for it (see below); otherwise every existing link and bookmark 404s.\n\n## Content & component transforms\n\nRewrite each page's MDX:\n\n- **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type=\"x\">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. A callout with an `icon` or `color` (Mintlify's generic `<Callout icon=\"key\" color=\"#FFC107\">`) stays a `<Callout>` instead, since a directive can't set them: Blume's takes `type`, `title`, `icon` (remap it with the table below), and `color` (any CSS color); drop `iconType`.\n- **Accordions — container/item inversion!** Mintlify nests `<Accordion title=\"…\">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title=\"…\">`→`<AccordionItem title=\"…\">` (item).\n- **`<Tabs>` → `<Tabs inline>`.** Mintlify renders tabs **borderless** — a strip on a full-width rule with the content flowing beneath as prose — while Blume's `<Tabs>` defaults to a bordered box. Add `inline` to each `<Tabs>` to preserve Mintlify's appearance; every child `<Tab title=\"…\">` is unchanged. Don't add `param` — Mintlify tabs switch in place and don't deep-link to the URL, so plain `inline` is the faithful mapping.\n- **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tab>`, `<Steps>`/`<Step>`, `<View>` (its `title` becomes an option in the page's view picker, as on Mintlify; remap its `icon` with the table below and drop `iconType`). Keep them as-is.\n- **Code-block titles carry over.** Both read every bare word after the language as the title (` ```ts Acme SDK `), and `wrap` and `expandable` mean the same in both. Mintlify's other fence options don't: `lines` → `lineNumbers` (left as is, it does nothing and warns `BLUME_CODE_FENCE_OPTION`), `highlight={1-2,5}` → `{1-2,5}`, `focus={2}` → a `// [!code focus]` comment on each line, and drop `icon=\"…\"`.\n- **`<RequestExample>`/`<ResponseExample>` pass through.** Top-level ones pin to a column beside the page on wide screens, request above response, as on Mintlify; `dropdown` works the same.\n- **Hand-written endpoint pages pass through.** `api: \"POST /v1/users\"` frontmatter, with `authMethod` (`bearer`/`basic`/`key`/`none`) and `playground` (`interactive`/`simple`/`none`), works as on Mintlify: the page's `<ParamField>`s build its Try it panel and request samples. Keep them as written.\n- **API fields pass through.** `<ParamField>` (`path`/`query`/`header`/`body`, `type`, `required`, `deprecated`, `default`, `placeholder`) and `<ResponseField>` (`name`, `type`, `required`, `deprecated`, `default`, `pre`, `post`) render natively, nested `<Expandable>` fields included. Keep them as written. An older `<RequestField>` isn't supported: rewrite it as a `<ParamField>`.\n- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `githubReleases()` source.\n- **Snippets become includes.** Blume has no `/snippets` import mechanism; its reuse is `<include>`. Move each snippet into the content folder as a `_`-prefixed partial (`_snippets/x.mdx`, which never renders as a page), and replace `import X from \"/snippets/x.mdx\"` + `<X prop=\"v\" />` with `<include prop=\"v\">./_snippets/x.mdx</include>` on its own line. In the partial, rewrite each `{prop}` placeholder to `{{prop}}`: include attributes are props read with the variables syntax. Named string imports (`import { foo } from \"/snippets/vars.mdx\"`) → a site-wide `variables` entry read as `{{foo}}`, or inline the value.\n\n## Frontmatter\n\nMintlify page frontmatter → Blume's strict schema. **`scripts/mintlify-codemod.mjs --write` does the mechanical rows automatically** (rename, drop, icon-remap) and flags the judgment rows (`openapi`/`asyncapi`) for you — see [Icons](#icons-fontawesome--lucide-required) for the invocation. The table is both the mapping reference and a description of exactly what the codemod does:\n\n| Mintlify | Blume | Codemod |\n| --- | --- | --- |\n| `title` / `description` | pass through | — |\n| `sidebarTitle` | `sidebar.label` | renames |\n| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |\n| `tag` | `sidebar.badge` | renames |\n| `canonical` | `seo.canonical` | renames |\n| `og:image` | `seo.image` | renames |\n| `hidden: true` | valid top-level in Blume — **kept as-is**; add `noindex: true` yourself if the page must also leave the search index | left (do by hand) |\n| `openapi`/`asyncapi` | usually an endpoint stub → **delete the page** (Blume generates operation pages); else drop the key and keep it as a normal page | **flags** for review — never auto-deletes a page |\n| `api`, `authMethod`, `playground` | kept as written (a hand-written endpoint page, see above) | left |\n| `related` (a list) | kept as written: root-relative paths, URLs, and `{ Title: link }` entries render as Related pages cards | left; `related: true` has no equivalent (Blume has no automatic related pages), so drop it (report) |\n| `mode` | `mode`, as written | `default`, `wide`, `center`, `custom`, and `frame` carry over; `assistant` has no equivalent → drop (report) |\n| `hideFooterPagination: true` | `pagination: false` | renames (`false` drops, since the links show by default) |\n| `keywords`, `boost` | `search.keywords`, `search.boost` | renames |\n| `searchable: false` | `search.exclude: true` | renames (`searchable: true` drops, since pages are searchable by default) |\n| `public`, `rss`, `groups`, `hideApiMarker`, `iconType` | **drop** (report) | drops |\n\nThe codemod leaves the source key in place and reports a conflict rather than clobbering data when a rename target already exists (e.g. a page already has `sidebar.label`) or the value is too structured to move safely — resolve those by hand. Remove any duplicate H1 in the body — `title` renders the H1. (The codemod only edits frontmatter; it never touches the body.)\n\n## OpenAPI\n\nTop-level `openapi`, `api.openapi`, or a per-group/per-tab `openapi` → `reference: [openapi({ sources: [{ spec, label?, route? }] })]`, with `openapi` imported from `blume/reference` (`spec` alone is the single-source shorthand; several per-tab specs become several sources, or several `openapi()` entries when they need different routes; a spec the source embedded with Scalar becomes a `scalar({ spec })` entry). A Mintlify `{ source, directory }` object: `directory` → the source's `route`. An `asyncapi` field maps the same way to `asyncapi({ … })` in the list. **Delete every per-endpoint stub page** (frontmatter `openapi: \"GET /path\"`, `openapi: \"spec.json webhook name\"`, or a `\"GET /path\"` nav entry) — Blume's native renderer generates one real page per operation and per webhook, and shows callbacks on their operation's page. **Add a `navigation.tabs` entry pointing at the reference `route` yourself** (Mintlify's API tab maps to it); the reference does not create a header tab automatically.\n\n- **Vendor the spec.** Mintlify usually points at a spec **URL**. Copying that straight into `spec:` makes every build fetch it at build time — a single point of failure in CI/offline/behind a proxy, and a failed fetch silently drops the reference (leaving the tab pointing at a route that 404s). Prefer downloading it into the repo (`curl … -o openapi/<name>.json`) and pointing `spec` at that local path. If you keep the URL, report the dependency and consider a `prebuild` refresh-with-fallback.\n- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/list-models`: a camelCase tag or id is split into kebab-case, not just lowercased — see SKILL.md \"OpenAPI\") — this differs from Mintlify's endpoint URLs, so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route and flags the ones you miss.\n- **Overlays carry over.** An `openapi: { source, overlays: [...] }` object → that source's `overlays` (`openapi({ spec, overlays })`, or `overlays` on each entry in `sources`), in the same order; Blume applies Overlay 1.0 and 1.1 the same way. An overlay Mintlify found only through its `extends` field (auto-discovered, not listed in `docs.json`) must be listed explicitly: Blume doesn't scan for overlays.\n- **Code samples carry over.** `api.examples.languages` → `openapi({ codeSamples: [...] })`; Blume generates the same 18 languages and accepts Mintlify's keys and aliases (`bash`, `javascript`, `node`, `csharp`, `dotnet`, `c++`, …) as written. `api.examples.autogenerate: false` → `codeSamples: false`. `x-codeSamples` in the spec needs nothing: Blume renders them ahead of the generated samples.\n- **Keep the \"Introduction\" page.** Mintlify commonly has a written intro/auth page in an \"Introduction\" group beside the \"Endpoints\" (openapi) group in the same tab. Keep it: a normal content page placed under the `openapi()` adapter's `route` (e.g. `<root>/api-reference/introduction.mdx`) merges into the reference tab's sidebar alongside the generated operations. Delete only the per-endpoint stubs, not the conceptual pages.\n\n## Assets\n\nMintlify serves every top-level dir (e.g. `/images`) at the site root. Blume serves `public/` at the site root. **Move root asset dirs into `public/`** (`mv images public/images`) — every `/images/...` reference still resolves, unchanged. Move loose root files (`logo.png`, `favicon.png`) under `public/` too.\n\n## i18n\n\n`navigation.languages` (≥2) → `i18n: { defaultLocale, locales: [{ code, label }] }`. The `default: true` language → `defaultLocale`. Translated content already lives in ISO-code directories, which match Blume's `dir` parser — no file moves. Remove any language selector; language switching is handled by i18n.\n\nPer-language `banner`, `navbar`, and `footer` (on `navigation.languages[]`) merge into the one site-wide config, with each label as a map of language code to that language's text: `banner: { content: { en: \"…\", fr: \"…\" } }` (and `link.text`), `navigation.actions`/`cta` labels, and `footer` link labels. Hrefs stay single — Blume moves an internal link into the reader's language itself. A language without an entry shows the default language's text. Report what doesn't merge: a banner or link set for only some languages, or one whose href differs per language.\n\n## Dropped — report these\n\n- **A group's `boost`** (on a `navigation` group, inherited by its pages) → no inherited boost; set `search.boost` on the pages that need it.\n- **Authentication** (password or SSO, set in the Mintlify dashboard) → Blume has none of its own; point the user at host-level protection (Vercel Deployment Protection, Netlify password protection, Cloudflare Access), per the deployment docs' **Private docs** section. It covers the whole site, so a mix of public and private pages needs two sites.\n- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`.\n","isInternal":false,"tokens":8430,"sizeBytes":31205},{"name":"mkdocs.md","path":".agents/skills/blume-migrate/references/mkdocs.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/mkdocs.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# MkDocs (Material for MkDocs, Zensical) → Blume\n\nMkDocs is a Python generator: one YAML config (`mkdocs.yml`), Markdown in `docs/`, and Python-Markdown extensions for everything beyond CommonMark. Nearly every MkDocs site uses the **Material for MkDocs** theme, so pages are full of extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets, `{ #id }` attribute lists), and navigation is an explicit `nav` list in config (or alphabetical). The work is translating `mkdocs.yml`, rebuilding `nav` as folders and `meta.ts`, and converting the syntax — most of which becomes `.mdx`-only Blume features, and **MDX rejects several things Python-Markdown accepts**.\n\nScope: checked against MkDocs 1.6.1, Material for MkDocs 9.7.6–9.7.7, pymdown-extensions 11.0.1–12.1, mkdocs-redirects 1.2.3, awesome-pages 2.10.1, awesome-nav 3.3.0, literate-nav 0.6.3, mkdocs-static-i18n 1.3.1, mkdocs-macros 1.5.0, mike 2.2.0, an end-to-end migration of astral-sh/uv (84 pages), and the codemod on pdm-project/pdm (23 pages). **Zensical** and **ProperDocs** projects are the same migration. MkDocs 2.0 (a pre-release with a TOML config and no plugins) was not checked.\n\n## Run the codemod first\n\nFrom the folder that holds `mkdocs.yml` (keep the old Python setup until the end, so `mkdocs build` can still produce the old site for comparison):\n\n```bash\nnode <skill>/scripts/mkdocs-codemod.mjs docs           # dry run: report only\nnode <skill>/scripts/mkdocs-codemod.mjs --write docs   # apply\n```\n\nIt reads `mkdocs.yml` (or `zensical.toml`/`properdocs.yml`, `INHERIT` included) for the enabled extensions, the snippets `base_path`, `extend_pygments_lang`, and `nav` titles, then per page: converts admonitions, details, and tabs (indentation-aware, nested); turns snippets into `<include>` (copying ones from outside the content root into `_snippets/`); rewrites fences, `#!lang` inline code, heading ids, attribute lists, keys, `==mark==`/`^^ins^^`, icon shortcodes, `\\(x\\)` math, and abbreviation lines; moves the body H1 (ATX or setext) into `title` (inline Markdown stripped — Blume renders backticks in `title` literally), a differing `nav` title into `sidebar.label`, and a differing frontmatter `title` into `seo.title`; maps `hide`/`status`/`tags`/`icon`; renames to `.mdx` **only** the pages that now need MDX and fixes their comments, autolinks, and void tags; renames `README.md` to `index.md`; and rewrites links and `<include>`s to every renamed file. It is idempotent.\n\nIt **reports**, without changing, what needs judgment: mkdocstrings blocks, macros/Jinja, template overrides, hooks, every plugin (with a hint), mike and `extra.alternate`, snippet line ranges, `///` blocks, grid cards, code annotations, unknown icons, raw `<img>`s with a relative `src`, pages left with no title, snippet files that publish as pages too, `nav` entries with no file, and every MDX hazard left in a renamed page. Work through that report with the sections below. Nav, config, redirects, and repo integration stay yours.\n\nAfter the first `blume build`, pin the old heading ids (MkDocs and Blume slug headings differ — see URLs). Point `--old` at the old site's built HTML (`mkdocs build -d <dir>` before you delete `mkdocs.yml`) or its live URL, and run it from the Blume project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url>           # dry run\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url> --write\n```\n\nRebuild and rerun until it reports 0 (add `--map dist/blume-redirects.json` when pages moved).\n\n## Detect\n\n- **`mkdocs.yml` / `mkdocs.yaml`** — Material when `theme.name: material`; the plain `mkdocs`/`readthedocs` themes migrate the same way with fewer extensions. Sometimes in a subfolder (`docs/mkdocs.yml`), which `blume migrate` doesn't detect from the repo root: name the source, and run the codemod from that folder.\n- **`zensical.toml`** — Zensical's format: MkDocs' keys in TOML, usually under `[project]` (`[project.theme]`, `nav = [ { \"Title\" = \"page.md\" } ]`); extensions as `[project.markdown_extensions]` keys (`admonition = {}`, `pymdownx.tabbed.alternate_style = true`) or `[project.markdown_extensions.<name>]` tables. With **no extensions configured**, Zensical enables a default set (admonition, details, tabbed, attr_list, md_in_html, keys, mark, caret, arithmatex, emoji, and more, but not snippets); the codemod applies it to a `zensical.toml`. Zensical also builds a plain `mkdocs.yml` and applies the same defaults when it sets no `markdown_extensions`: the codemod can't tell, so list the extensions the pages use in a copy of the config and pass it with `--config`.\n- **`properdocs.yml` / `.yaml`** — ProperDocs, a continuation of MkDocs 1.x.\n- Python deps, not `package.json`: `mkdocs`, `mkdocs-material`, `zensical`, `properdocs` in `requirements*.txt` or a `pyproject.toml` docs group; `mkdocs build`/`gh-deploy` in CI; `.readthedocs.yaml` with an `mkdocs:` key.\n\nResolve the config first: `INHERIT: base.yml` deep-merges this file over another (a repo may have several configs on one base — migrate the public one, report the rest). `!ENV [VAR, default]` reads an environment variable: use the default and report it. `!relative` and `!!python/name:` tags break JS YAML parsers — read the file as text.\n\n**Where the Blume project goes.** `blume build` empties `<project>/dist`, and a Python or Rust repo root often uses `dist/` for wheels or builds. When `mkdocs.yml` sits at the root of a code repo, put `blume.config.ts` and `package.json` **in the docs folder** with `content.root: \".\"` and `content.include` scoped to the page folders (`[\"index.mdx\", \"{guides,concepts,reference}/**/*.{md,mdx}\"]`), and say so. That also keeps a `package.json` out of a non-JS repo root.\n\n**Generated pages.** A `nav` entry or link whose file isn't in the repo is generated at build time (`gen-files`, a hook, a CI step, a `.gitignore`d output — the codemod lists them). Convert today's output once so the site builds, then report that the **generator must change**: until it writes Blume syntax, CI regenerates the old one and Blume renders it as text with a green build. If conversion renames the output (`settings.md` → `settings.mdx`), update `.gitignore`, `.prettierignore`, and the generator's own check mode too.\n\n## Config: `mkdocs.yml` → `blume.config.ts`\n\nMap only what's set:\n\n| MkDocs | Blume |\n| --- | --- |\n| `site_name` / `site_description` | `title` / `description` |\n| `site_url` | Its origin → `deployment.site` (unless the target is Vercel or Netlify — see SKILL.md); **a path in it** (`https://docs.acme.com/uv/`) → `deployment.base: \"/uv\"`, on every host. mike adds its version segment after `site_url`, so it isn't part of the base (Versioning) |\n| `docs_dir` | `content.root` (`docs` is the default) |\n| `use_directory_urls` | no setting — see URLs |\n| `repo_url` on GitHub | `github: { owner, repo }` |\n| `edit_uri` / `edit_uri_template` | **`github.branch`** from the segment after `edit/` or `blob/`. Unset with a GitHub `repo_url` means `edit/master/docs/` → `branch: \"master\"` (Blume defaults to `main`). The path after the branch is `docs_dir`, which Blume adds — **not** `github.dir` (that's only for a project in a repo subfolder). `edit_uri: \"\"`, or Material without the `content.action.edit` feature, shows no edit button; Blume has no separate switch, so accept edit links or leave `github` unset and set `navigation.repo: \"<repo_url>\"` (a footer mark) — say which. Leave `github` unset if edit links would point at gitignored generated files |\n| `repo_url` elsewhere (GitLab…) | no edit links; a header link → `navigation.actions` (report) |\n| `repo_name`, `site_author`, `site_dir`, `dev_addr`, `remote_*`, `watch`, `strict`, `validation` | drop |\n| `copyright` | `footer.copyright`, plain text: strip any HTML tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`) |\n| `exclude_docs` | `content.exclude` — the list adds to the default `\"**/_*\"` and `\"**/.*\"`; `\"!**/_*\"` publishes `_` files |\n| `draft_docs` / `not_in_nav` | `draft: true` / `sidebar.hidden: true` on those pages |\n| `extra_css` | a project-root **`theme.css`**: Material's `--md-*` variables and `.md-*` classes don't exist — port only rules that still mean something, and read colors out of it (Theme) |\n| `extra_javascript` | delete MathJax/KaTeX/Mermaid loaders. Anything else → move into `public/` and `script({ src })` in `analytics` (from `blume/analytics`); a root-relative `src` (`\"/js/x.js\"`) loads under `deployment.base`. Code written against Material's `document$` or `.md-*` DOM won't run — report it |\n| `extra.social` | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube; `twitter`/`x-twitter` → `x`); the site's own repo is covered by `github`; other platforms drop (report) |\n| `extra.analytics` (`provider: google`) | `googleAnalytics({ id: property })`. A `custom_dir` partial `partials/integrations/analytics/<name>.html` is a custom provider: use its adapter (`fathom({ site })` from `data-site`, …) or `script()`. `feedback` → Blume's page feedback is on by default |\n| `extra.consent` | `consent: native()` (from `blume/consent`) |\n| `extra.version` (`provider: mike`) | Versioning |\n| other `extra` keys used as `{{ }}` | `variables` (Manual decisions: macros) |\n| `markdown_extensions` | nothing to configure. `toc.toc_depth: N` → `toc: { maxHeadingLevel: N }` (a `\"2-5\"` range: its upper bound) |\n| `plugins`, `hooks`, `theme.custom_dir` | Plugins; Manual decisions |\n\n### Theme\n\n| Material | Blume |\n| --- | --- |\n| `theme.palette` | one palette with `scheme: slate` → `theme.mode: \"dark\"`; with `default` → `\"light\"`; a list with toggles → system (the default) |\n| `palette.primary` (link color) | `theme.accent`. Presets `red`, `pink`, `purple`, `blue`, `green`, `orange`, `teal` map by name. Other names aren't usable CSS (`deep purple` warns `BLUME_THEME_COLOR_INVALID`, CSS `indigo` isn't Material's): use Material 9.7's link color (`--md-typeset-a-color`) as `{ light, dark }` — indigo `#4051b5`/`#5488e8`, deep purple `#7e56c2`/`#a47bea`, light blue `#02a6f2`, cyan `#00bdd6`, deep orange `#ff6e42`/`#ff764d`, brown `#795649`/`#c1775c`, amber `#d19d00`/`#ffc105`, lime `#8b990a`/`#cbdc38`, light green `#72ad2e`/`#8bc34b`, yellow `#b8a500`/`#ffec3d`; grey, blue grey, black, and white all link in `#4051b5`/`#5e8bde`. `custom` or a custom `scheme:` is defined in `extra_css`: take `--md-typeset-a-color` per scheme (and `--md-default-bg-color` → `theme.background` when it isn't plain white/black) |\n| `palette.accent` | drop |\n| `theme.font.text` / `.code` | `theme.fonts.body` (and `display`) / `mono` as `{ name: \"…\" }`; `font: false` → drop |\n| `theme.logo` (a file in `docs_dir`) | move into `public/`, `logo: \"/logo.svg\"` (SKILL.md on `currentColor` and wordmarks). `theme.icon.logo` → drop and report |\n| `theme.favicon` | move to `public/favicon.<ext>` — no config field |\n| `theme.icon.*`, `theme.language` (English) | drop. A non-English single-language site: the schema accepts a one-locale `i18n` block — check the UI language in `blume dev` |\n\n`theme.features`:\n\n| Feature | Blume |\n| --- | --- |\n| `navigation.tabs` | **`navigation.tabs`**, one per top-level `nav` section, each in one folder (SKILL.md \"Tabs\") |\n| `navigation.sections` | top-level sections stay Blume's default `flat` headers; set **`display: \"group\"` in the `meta.ts` of every second-level section**: Material renders those as collapsible, and a `flat` group lists its pages above its subgroups, reordering a section that interleaves them |\n| neither `sections` nor `tabs` | `navigation.sidebar.display: \"group\"` (Material's collapsible tree) |\n| `navigation.expand` | `collapsed: false` in those folders' `meta.ts` |\n| `navigation.indexes`, plugin `section-index` | a folder's `index` page links its group header. Blume also lists it as the group's first row, so the label shows twice. To show it once, as Material does, set `sidebar.hidden: true` on the index: that drops only the duplicate row, and the page stays in `sitemap.xml`, search, and `llms.txt`. A hidden index whose `title` differs from its folder's `meta.ts` `title` warns `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`: match the two titles, set the index's `sidebar.label` to the folder title to keep its heading, or leave that index's row showing |\n| `content.tabs.link` | same-titled tabs sync within a page, not across pages |\n| `content.action.edit` / `.view` | `github` |\n| `announce.dismiss` | `banner.dismissible`; the text is `main.html`'s `{% block announce %}` → `banner.content` |\n| `content.code.annotate` | no equivalent (the codemod reports annotation markers) |\n| `content.code.copy`/`.select`, `navigation.footer`/`.path`/`.top`/`.instant*`/`.tracking`/`.prune`, `toc.*`, `search.*`, `header.autohide`, `content.tooltips` | drop — built in or no equivalent. Blume's copy button copies only the commands of a `console` or `shellsession` block, without prompts or output, so a custom script that stripped them (`extra_javascript`) can go once those blocks use one of the two |\n\n## Navigation\n\nBlume builds the sidebar from folders (SKILL.md \"Navigation is the file tree\"). Translate `nav` into folders and `meta.ts` (the codemod already set each page's `title`/`sidebar.label`):\n\n- **`- Section: [ … ]`** → a folder; a label that differs from the humanized folder name (\"Getting started\" vs \"Getting Started\") → `meta.ts` `title`. List order → `meta.ts` `pages`, every child. Root order → the content root's `meta.ts`.\n- **A section holding an `index.md`** (with `navigation.indexes`) has that page as its own → nothing to do (see `navigation.indexes` above). Without the feature, Material lists the index as an ordinary row, which is what Blume does anyway.\n- **Nesting that doesn't match folders** is SKILL.md's \"Config-declared nesting\". Decide per section and report:\n  - **One flat folder split into subsections** (`Advanced:` listing `guides/webhooks.md`): a **`(group)/` folder** first, as SKILL.md says — `guides/(advanced)/webhooks.md` keeps `/guides/webhooks` and adds the sidebar group, with no redirects.\n  - **A folder shown under another section** (uv's `pip/` under \"Concepts\"), where a group folder would change the URL segment: **move the files and pin `slug: <old route>` on each moved page**. The sidebar follows the file and the URL follows the slug, with no redirects and no explicit sidebar. Use it when the source kept URLs on purpose.\n  - Otherwise, move the files and add `redirects` (the URLs change), or declare the shape in an explicit `navigation.sidebar` (it replaces the whole generated tree).\n  - Either move puts a page a folder deeper: re-base its relative links and assets, and rewrite every link to it to name the file as it now is (a link to `page.md` resolves to a slugged page only when it names that exact file).\n- **`- Title: https://…`** → `navigation.featured` (top level) or `navigation.actions`; a `nav` entry to a non-page file (`llms.txt`, a PDF) → a featured link.\n- **Pages left out of `nav`** → `sidebar.hidden: true` (note: this also drops them from `sitemap.xml`).\n- **No `nav`**: MkDocs lists each folder's pages (`index` first, then by filename) before its subfolders, labeled \"Getting started\"-style; Blume sorts by title. Add `meta.ts` `pages`/`title` where that differs.\n- **`awesome-pages` `.pages` / `awesome-nav` `.nav.yml`** → a `meta.ts` per folder: `title` → `title`; `nav` → `pages` (drop awesome-pages' `...` and awesome-nav's glob entries like `\"*\"`; an awesome-pages `nav` without `...` hides everything unlisted → `sidebar.hidden: true`; `- Title: page.md` → `sidebar.label`); `hide: true` → hide its pages; sorting, `append_unmatched`, and `collapse*` options drop (report). Delete the files.\n- **`literate-nav`** (`SUMMARY.md`): its nested link list is the nav — convert it like `nav` and delete it (it would publish as a page).\n\n## URLs and redirects\n\nSave the old URL list first — every `<loc>` in the old `site/sitemap.xml` (or the live one) plus every `redirect_maps` key — and check it at the end.\n\n| Source file | MkDocs URL (`use_directory_urls: true`) | (`false`) | Blume route |\n| --- | --- | --- | --- |\n| `docs/a/b.md` | `/a/b/` | `/a/b.html` | `/a/b` |\n| `docs/a/index.md` | `/a/` | `/a/index.html` | `/a` |\n| `docs/a/README.md` | `/a/` | `/a/index.html` | `/a/README` until renamed `index.md` (the codemod does) |\n| `docs/01-setup.md` | `/01-setup/` | `/01-setup.html` | **`/setup`** — Blume strips numeric prefixes; redirect, or pin `slug: 01-setup` |\n\n- **Trailing slashes**: a static build writes `<route>/index.html`, so slashed URLs keep working on static hosts; `vercel()` 308s them; Cloudflare Workers needs `html_handling: \"drop-trailing-slash\"`. `blume dev` and `blume preview` redirect a slashed URL to the slashless page, so old URLs can be checked with or without the slash. **Write targets in any client-side script** (anchor-redirect maps) slashless: Blume's URLs have none.\n- **`use_directory_urls: false`**: one exact redirect per non-index page (`{ from: \"/a/b.html\", to: \"/a/b\" }`; a pattern can't strip a suffix). Hosts that read `_redirects`/`vercel.json` answer them; GitHub Pages, which reads neither, is unverified.\n- **`redirects` plugin**: each `redirect_maps` key and value is a Markdown path relative to `docs_dir` → `{ from: <route of key>, to: <route of value> }`, `#anchor` kept; an `http(s)` value stays a URL.\n- **Anchor-level redirects** (a script mapping `/old/#section`): servers never see fragments. Keep the script (`script({ src })`, targets slashless) or drop it; report either way.\n- **Heading ids**: MkDocs' default slugify strips accents, collapses punctuation runs (`A & B` → `a-b`), and numbers duplicates `_1`; Blume keeps accents, writes `a--b`, and numbers `-1`. Run `pin-heading-ids.mjs` (above) — don't compare by hand.\n- **Links**: the codemod rewrites links to renamed files. After you move or rename anything yourself, point links at the real filenames again — a `page.md` link to a file now named `page.mdx` still lands on its page (Blume tries the other extension, `slug` included), but breaks wherever the Markdown is read as files, such as on GitHub. Links inside an included partial resolve from the **including** page, as with MkDocs snippets. A link to a file moved into `public/` becomes root-relative. A link MkDocs couldn't map to a file (`../install/`, written against MkDocs' slashed URLs) lands one level higher in Blume on any page that isn't an `index`, because Blume reads relative links from the page's folder: point it at the `.md` file. `blume validate --strict` catches the rest. The codemod percent-encodes angle-bracket destinations (`[x](<https://…(…)>)` → `…%28…%29`), which only tidies: Blume reads both forms, and rebases a partial's `<…>` image path onto the including page.\n- **llms.txt plugins**: Blume writes `/llms.txt` and `/llms-full.txt`. The plugin's `/<route>/index.md` copies become `/<route>.md`: add one exact redirect each (a pattern can't map them; `blume audit --only redirects` accepts `.md` targets), and reword any URL advice carried into `agents.llmsTxt.details`.\n\n## Which pages become `.mdx`\n\nDirectives, components, math, and Mermaid work only in `.mdx`, and so does an `<include>` indented inside a list item (a `.md` page reads it as indented code and ships the bare tag); the codemod renames exactly the pages that use them (or include a partial that does) and keeps the rest `.md`. MDX **fails the build** on Python-Markdown-valid source, and the codemod reports what's left in renamed pages: `<!-- -->` comments (→ `{/* … */}` on **one line**), `<https://…>` autolinks, unclosed `<br>`/`<img>`, `<placeholder>` text in prose (`uv run <command>` → backticks), and any `{…}` in prose — attribute lists, critic markup, Jinja, `{name}`. Most fail to compile, and `blume check` (and `validate`, `dev`, `build`) names each at its line as `BLUME_MDX_SYNTAX`; it also names an attribute list (`BLUME_MDX_ATTRIBUTE_LIST`: move the attributes onto a JSX element, or drop them) and an unclosed void tag (`BLUME_MDX_UNCLOSED_ELEMENT`: close it, `<br />`). `{ width=\"300\" }`, `{target=\"_blank\"}`, and `{name}` compile and then fail the render, which `blume build` reports as `BLUME_MDX_UNDEFINED_NAME` at the expression's line. In a `.md` page none of these fail and no extension syntax renders: it ships as text (`--8<--` becomes \"–8<–\", `*[HTML]: …` shows verbatim), and an attribute list warns `BLUME_MD_ATTRIBUTE_LIST`. Smart punctuation turns prose `--flag` outside backticks into an en dash.\n\n## Content\n\nWhat the codemod converts, for checking its output, and what it leaves for you:\n\n| MkDocs | Blume |\n| --- | --- |\n| `!!! type \"Title\"` (body indented 4) | `:::type[Title]` … `:::` (longer `::::` outside nested ones). Material styles 12 types; others render like `note`. Mapped: `abstract`/`question` → `info`, `failure`/`bug` → `danger`, `example`/`important`/`todo` → `note`, `caution`/`attention` → `warning`, `hint` → `tip`, `quote` → a blockquote. Untitled callouts lose Material's \"Note\" title bar; a bracket title keeps its inline Markdown (an unbalanced `]` flattens it) |\n| `??? type \"Title\"` / `???+` | `<Expandable title=\"Title\">` / `defaultOpen`; the type's color drops, and the title keeps its inline Markdown (the codemod strips raw HTML, which would show as text) |\n| `=== \"Tab\"` | every tab one code block → `<CodeGroup>` with the label as `title=\"…\"`; otherwise `<Tabs>`/`<Tab title>` (`===+` → `defaultTabIndex`, `===!` → a new group) |\n| `hl_lines=\"2 4-5\"`, `linenums=\"1\"`, `{ .py title=\"x\" }`, options without a language | `{2,4-5}`, `lineNumbers` (a start other than 1 drops), `py title=\"x\"`, `text {…}`. One left over warns `BLUME_CODE_FENCE_OPTION` (with the Blume spelling) or `BLUME_UNKNOWN_CODE_LANGUAGE` |\n| `pycon`, `pwsh-session`, `extend_pygments_lang` names | `python`, `powershell`, the mapped lexer (any other name Shiki lacks warns `BLUME_UNKNOWN_CODE_LANGUAGE`: map it or write `text`) |\n| `` `#!python x()` `` | `` `x(){:python}` `` |\n| `{ #id }`, `{: #id }`, `{#id}` on headings, also beside classes (`{ #id .wide }`) | `[#id]`, classes dropped (a self-link like `### [x](#x) {: #x }` drops). One left over warns `BLUME_MD_CURLY_ANCHOR` in `.md` (only `{#id}` pins there), and `BLUME_MDX_CURLY_ANCHOR` in `.mdx` (`BLUME_MDX_ATTRIBUTE_LIST` beside classes): write `[#id]` |\n| other attribute lists | dropped and reported (one left over warns `BLUME_MD_ATTRIBUTE_LIST` in `.md`): buttons → links or `<Card>`s (a homepage CTA → `navigation.cta`); image sizing → `<img … />` or `<Frame>`; `{target=\"_blank\"}` → `markdown.externalLinks: true` |\n| `++ctrl+c++`, `==x==`, `^^x^^` | `<kbd>Ctrl</kbd>+<kbd>C</kbd>`, `<mark>`, `<ins>` |\n| `:material-check:` (mapped icons) | `<Icon icon=\"check\" />`; `:octicons-link-external-16:` drops (see `externalLinks`) |\n| `\\(x\\)`, `\\[`…`\\]` | `$$x$$`, `$$` blocks |\n| `*[HTML]: …`, `[TOC]` | removed |\n| `<div markdown>` | attribute dropped (Markdown inside renders in `.mdx`) |\n| Body `# H1` | frontmatter `title` |\n| **By hand:** single-`$` math | `$$…$$` inside the sentence, where it is math |\n| definition lists | `<dl>` or a list |\n| `///` blocks (pymdownx.blocks) | the same targets as `!!!`/`???`/`===`; `/// caption` → `<Frame caption>` |\n| grid cards (`<div class=\"grid cards\" markdown>`) | `<CardGroup>`/`<Card>` |\n| code annotations (`# (1)!` + a list) | prose after the block |\n| `![](x#only-light)` / `#only-dark` | a `theme.css` rule: `:root:not([data-theme=\"dark\"]) img[src$=\"#only-dark\"], :root[data-theme=\"dark\"] img[src$=\"#only-light\"] { display: none; }` (checked in a browser) |\n| emoji `:smile:`, magiclink `#123`/`@user` | paste the emoji; write the links |\n| unmapped icons | a Lucide name in `<Icon>` (verify at lucide.dev), or drop; brand icons (Python, Linux, Docker) have none |\n\n### Snippets and includes\n\n- Snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), never the page; the codemod rewrites them to `/`-rooted `<include>`s.\n- Includes must live in the content root. The codemod copies outside files (`CONTRIBUTING.md`, `examples/`) into `_snippets/`; if the source keeps changing, ship a small `predev`/`prebuild` script that re-copies it, and gitignore the copy. Drop a copied file's H1 (the codemod reports it) — the page's title already renders one.\n- Line ranges (`file.py:3:10`) have no `<include>` equivalent: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (`lines`, run before every build so it keeps tracking the source). Sections (`file.md:name`, marked `--8<-- [start:name]`) aren't read by that script: split the section into its own file and report it.\n- `auto_append` link-definition files: `<include>` the file at the end of each page that uses them. An included file's definitions resolve references in the including page, as if written inline (and the page's resolve in the partial).\n- A partial is parsed with the page that includes it: its directives render only in an `.mdx` page. An HTML comment in a `.md` partial stays hidden in an `.mdx` page too.\n- A snippet file that isn't under a `_` or `.` folder or name is a page too, and publishes on its own as it did in MkDocs; the codemod reports each one. Move it under `_snippets/` unless it should stay a page.\n- `include-markdown` (`{% include \"./x.md\" %}`): `./` paths are page-relative like Blume's, bare ones `docs_dir`-relative (→ `/x.md`); `start`/`end` excerpts → their own file.\n\n## Frontmatter\n\nThe codemod maps `hide: [toc]` → `mode: wide`, `hide: [navigation]` → `mode: center`, `hide: [footer]` → `pagination: false`, `status: new` → `sidebar.badge: New` (`deprecated` → `deprecated: true`), `tags` → `search.tags` (a hosted-search facet; tag pages drop), `icon: material/…` → a Lucide name, and drops `subtitle`, `template`, `social`, `comments`, `render_macros`. `search.exclude`/`boost` keep their meaning. It reports every other key Blume's strict schema rejects (`BLUME_FRONTMATTER_INVALID`) — map or remove them; `.meta.yml` defaults (the `meta` plugin) apply to every page in their folder.\n\n## Plugins — map, don't drop\n\n| Plugin | Blume |\n| --- | --- |\n| `search` | built in — delete |\n| `redirects` | `redirects` (URLs) |\n| `git-revision-date(-localized)` | `lastModified: \"git\"` (it shows a visible \"Last updated\" line) |\n| `git-committers`, `git-authors`, `minify`, `privacy`, `offline`, `typeset`, `info` | drop |\n| `social`, `glightbox`, `optimize` | built in: Open Graph images, image zoom, image optimization |\n| `group` | unwrap and map its plugins |\n| `exclude` | `content.exclude` |\n| `meta`, `tags` | Frontmatter |\n| `blog`, `rss` | Blog |\n| `awesome-pages`, `awesome-nav`, `literate-nav`, `section-index` | Navigation |\n| `include-markdown` | Snippets |\n| `llmstxt` | built in: `markdown_description` → `agents.llmsTxt.details` (reword URL advice); `sections` drop |\n| `macros`, `mkdocstrings`, `mkdocs-click`, `gen-files`, `mkdocs-jupyter` | Manual decisions |\n| `i18n`/`static-i18n`, `mike` | i18n; Versioning |\n| anything else | read what it does; report it |\n\n## Manual decisions — stop and report\n\n- **mkdocstrings** (`::: package.module`): no Python API generator in Blume. Keep that reference published elsewhere and link to it, use `openapi()` for an HTTP API, or hand-write the key pages; redirect the URLs you drop. The same for `mkdocs-click` CLI references and `gen-files` output.\n- **macros**: plain values (`extra`, `include_yaml`, `{% set %}`) → `variables`, same `{{ name }}` syntax — names take letters, digits, `_`, `-` only, and `{{ config.x }}`/`{{ page.x }}` must be inlined. `{% include 'x.md' %}` (from `include_dir`, default `docs_dir`) → `<include>/x.md</include>`. Macro calls and control flow → expand into plain content (a one-off render script is fine); strip `{% raw %}` wrappers.\n- **Template overrides** (`custom_dir`) and **hooks**: read each. Announcement → `banner`; `<head>` meta tags → `seo.metatags` (minus the tags Blume owns); analytics partials → adapters; `<title>`/JSON-LD tweaks → Blume's own; the rest → layout slots (`defineComponents`) or `blume eject`.\n- **Notebooks** (`mkdocs-jupyter`): export to Markdown or link out.\n\n## i18n\n\n`static-i18n` `languages` → `i18n: { defaultLocale, locales: [{ code, label }] }` (both serve the default at the root, others under `/<locale>/`). `docs_structure: suffix` (`page.fr.md`, the default) → `parser: \"dot\"`; `folder` → the default `dir` parser with the **default locale's folder moved up to the content root**. `nav_translations` → per-locale `meta.ts` titles; `fallback_to_default` matches `fallbackLocale`. Pin translated headings with `[#id]`.\n\n## Versioning (`mike`)\n\nmike publishes built HTML per version (`/<version>/…`, aliases like `/latest/`); old versions' sources live only in git tags. Migrate the current version unless asked: serve it at the root and redirect `/<alias>/:path*` and `/<version>/:path*` to `/:path*`. Older versions stay on the old host (a `navigation.featured` link) or are converted from their tags into Blume's `versions.archived` (ids start with a letter). Report which.\n\n## Blog (Material `blog` plugin)\n\nPosts in `blog/posts/` → `type: blog` pages. `date: { created, updated }` → `date` + `lastModified`; `authors: [id]` → inline objects from `.authors.yml`; `categories` → `search.tags` or drop. Keep the old dated URL with a full-route `slug: blog/2024/01/31/my-post`, or redirect. Delete `<!-- more -->` in `.mdx`. Blume has no blog index, archive, category, or pagination pages: write `blog/index.mdx` (`docs/advanced/blog.mdx`), report the rest. RSS moves to `/blog/rss.xml`; with the `rss` plugin (mkdocs-rss-plugin), redirect its `/feed_rss_created.xml` and `/feed_rss_updated.xml` (and `/feed_json_*.json`) there.\n\n## Repo integration and teardown\n\n- **Assets**: everything non-Markdown in `docs_dir` that's served by URL — logo, favicon, downloads, `CNAME`, verification files — moves to `public/`. Images and other files that pages link relatively can stay: Blume publishes each with the page, though at a new URL, so a download whose old URL is linked from elsewhere moves to `public/` too.\n- **Renamed files**: after the codemod, **grep the whole repo for the old page paths** — release tooling that bumps versions inside pages (`[tool.rooster] version_files`, `bumpversion`), Renovate/Dependabot file globs, CODEOWNERS, `.prettierignore`, CI path filters, and scripts.\n- **Prettier**: `.prettierrc` and `.editorconfig` overrides keyed on `*.md` stop applying to the new `.mdx` files — extend them to `*.mdx` before reformatting. Prettier turns a multi-line `{/* … */}` into `{/_ … _/}` (keep MDX comments on one line) and rewrites a URL containing parentheses as a `<…>` destination (fine in a page; percent-encode it in a partial, see Links). If the repo uses Ultracite/oxfmt, see `references/monorepo.md` §6.\n- **CI**: replace `mkdocs build`/`gh-deploy` with Node 22, an install, `blume build`, and `blume validate --strict` (GitHub Pages: the \"Deploy Markdown docs to GitHub Pages\" guide); a `.readthedocs.yaml` build is a hosting change to report. Add `.gitignore` entries for `node_modules/`, `.blume/`, `dist/`, and `.env.local`.\n- **Teardown**: delete `mkdocs.yml` (and `INHERIT`ed bases), `overrides/`, `.pages`/`.nav.yml`/`SUMMARY.md`/`.meta.yml`, hooks, and `extra_css`/`extra_javascript` files once ported. Leave the Python docs dependencies for a follow-up: removing a `pyproject.toml` docs group needs a lock regeneration (`uv lock`), and CI running with `--locked`/`UV_LOCKED=1` fails until it's done — report it.\n\n## Leftover check\n\nRerun until it prints only what you mean to keep (code samples, and `{{name}}` variables you defined):\n\n```bash\ngrep -rnE '^\\s*(!!!|\\?\\?\\?\\+?|===[+!]*) |^\\s*/{3,} ?[a-z]|--8<--|\\{ *[.#:][^}]*\\}|\\{\\{|\\{%|<!--|:(material|octicons|fontawesome|simple)-[a-z0-9-]+:|\\+\\+[a-z]|==[^= ][^=]*==|\\{\\+\\+|\\{--|^\\*\\[|`#![a-z]|hl_lines=|linenums=' docs\n```\n\n## Dropped — report these\n\nTemplate overrides and hooks with no slot equivalent, links inside `copyright`, code annotations, admonition title bars and collapsible callout colors, `inline` admonition placement, `palette.accent`, unmapped and brand icons, abbreviation tooltips, definition-list styling, snippet line ranges and sections (split into files), tag index pages, blog archive/category/pagination pages, anchor-level redirects without the script, cross-page tab memory, git committers, mkdocstrings/click/gen-files/notebook output not carried over, macro functions and Jinja logic, social platforms `footer.socials` lacks, a `linenums` start offset, image sizing from attribute lists, and every plugin you removed without an equivalent.\n","isInternal":false,"tokens":9260,"sizeBytes":33161},{"name":"monorepo.md","path":".agents/skills/blume-migrate/references/monorepo.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/monorepo.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Migrating into a real repo (monorepos, pnpm, Vercel)\n\nThe framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.\n\n**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.\n\n---\n\n## 1. Non-standard content layout → scope `content.root` + `include`\n\n`content.root` defaults to `\"docs\"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.\n\n**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:\n\n```bash\n# top two path segments of every content file, deduped — your content folders\nfind . -path ./node_modules -prune -o \\( -name '*.md' -o -name '*.mdx' \\) -print \\\n  | sed 's|^\\./||' | cut -d/ -f1 | sort -u\n```\n\nIf that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.\n\n**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: \".\"` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nexport default defineConfig({\n  content: {\n    root: \".\", // content sits directly under this package (apps/docs/), no docs/ subfolder\n    // include/exclude globs are relative to `content.root`. List the actual\n    // content folders so the scan never wanders into src/, public/, etc.\n    include: [\n      \"api/**/*.{md,mdx}\",\n      \"getting-started/**/*.{md,mdx}\",\n      \"guides/**/*.{md,mdx}\",\n    ],\n    exclude: [\"**/node_modules/**\"], // added to the default \"**/_*\" and \"**/.*\"\n  },\n});\n```\n\nSchema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `[\"**/*.{md,mdx}\"]`), **`content.exclude`** (array of globs, relative to `content.root`, added to the default `[\"**/_*\", \"**/.*\"]`; a `!`-prefixed default, `\"!**/_*\"`, takes it back out). Singular `include`/`exclude`, both arrays. A folder `meta.ts` outside every `include` glob is never read (`BLUME_META_OUTSIDE_INCLUDE` names it), so a folder that holds only a `meta.ts`, like an API reference's tag folder, needs a glob of its own (`\"api-reference/**/*.{md,mdx}\"`).\n\nReport to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.\n\n---\n\n## 2. Release-age guards (pnpm `minimumReleaseAge`, Yarn `npmMinimalAgeGate`) block installing fresh Blume\n\npnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.\n\n**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.\n\n**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:\n\n```bash\ngrep -rn \"minimumReleaseAge\\|minimum-release-age\" pnpm-workspace.yaml .npmrc 2>/dev/null\n```\n\n**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):\n\n```yaml\nminimumReleaseAge: 1440\nminimumReleaseAgeExclude:\n  - blume\n```\n\n**Fix — `.npmrc`** form (if the policy lives there instead):\n\n```ini\nminimum-release-age=1440\nminimum-release-age-exclude[]=blume\n```\n\nReport: \"Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else.\"\n\n**Yarn ≥4.10** has the same guard: `npmMinimalAgeGate` in `.yarnrc.yml` (e.g. `2w`). Its per-package exemption is `npmPreapprovedPackages`. Yarn reads rc files from parent directories, so a nested `docs/` package inherits the root's gate. Detect with `grep -rn \"npmMinimalAgeGate\" .yarnrc.yml docs/.yarnrc.yml 2>/dev/null`, then add `npmPreapprovedPackages: [\"blume\"]` (or append `blume` to the existing list) in the file that sets the gate. Don't remove the gate.\n\n---\n\n## 3. Frozen lockfile — regenerate and commit the lockfile in the same change\n\nCI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.\n\n**After every dependency edit, regenerate the lockfile with the repo's package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, `bun install`) and stage it with the manifest.** The pnpm case:\n\n```bash\npnpm install            # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml\ngit add package.json apps/docs/package.json pnpm-lock.yaml\n```\n\nRules:\n\n- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.\n- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.\n- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors \"lockfile not up to date,\" you forgot the plain install.\n\n---\n\n## 4. Vercel monorepo recipe (root-aware install + build)\n\nA plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/` (for a static build; see d) for `vercel()`). `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.\n\nThis is a **copyable template**, not prose. Three pieces:\n\n**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"blume dev\",\n    \"build\": \"blume build\",\n    \"preview\": \"blume preview\"\n  },\n  \"dependencies\": {\n    \"blume\": \"^2\"\n  }\n}\n```\n\n**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"framework\": null,\n  \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\",\n  \"buildCommand\": \"pnpm run build\",\n  \"outputDirectory\": \"dist\"\n}\n```\n\n**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**\n\n- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.\n- **Node version** = 22.19 or newer (Blume requires it). Or pin it in the docs package: `\"engines\": { \"node\": \">=22.19\" }`.\n\nHow the pieces fit, with Root Directory = `apps/docs`:\n\n- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.\n- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.\n- `outputDirectory: \"dist\"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.\n- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.\n\nIf the repo uses **Turborepo**, you can instead build through the root pipeline — `\"buildCommand\": \"cd ../.. && pnpm turbo build --filter=docs\"` — for remote-cache reuse. Keep `outputDirectory: \"dist\"` and Root Directory `apps/docs`.\n\nReport every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.\n\n**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)\n\n**d) Server output (`deployment: vercel()`).** A `vercel()` build (needed for the assistant, the MCP server, Mixedbread search, or the playground proxy) doesn't leave a static `dist/` for Vercel to serve. It writes the Build Output API folder `.vercel/output` in the docs package, which Vercel deploys as is. Use the same `vercel.json` without `outputDirectory`: `{ \"$schema\": …, \"framework\": null, \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\", \"buildCommand\": \"pnpm run build\" }`.\n\n- Keep `framework: null`: it overrides a preset the project already has (Nuxt, Next.js), whose build settings would replace these.\n- Root Directory and Node are the same dashboard settings. Server secrets (`AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token) go in the project's environment variables.\n- With Turborepo, add `\".vercel/output/**\"` to the docs task's `outputs`.\n- The redirect caveat above doesn't apply: `vercel()` writes redirects into `.vercel/output/config.json`, so they're real 3xx responses.\n- `blume preview` can't serve this build; check it with `blume dev` and a preview deployment.\n\n---\n\n## 5. Astro/Vite version mismatch → pnpm patch (manual)\n\nBlume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.\n\n**Detect the risk factor** before it bites:\n\n```bash\ngrep -rn \"\\\"vite\\\"\\|vite@\\| vite:\" pnpm-workspace.yaml package.json 2>/dev/null | grep -i \"override\\|resolution\\|catalog\\|vite\"\n```\n\nBlume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):\n\n```bash\n# 1. Open the offending version in an editable temp dir (use the exact version\n#    pnpm resolved — check `pnpm why astro`):\npnpm patch astro@7.0.6\n\n# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface\n#    (the exact edit depends on the mismatch; the crash's stack trace names the\n#    file and call site). Then commit the patch:\npnpm patch-commit '<the temp path pnpm printed>'\n```\n\n`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  astro@7.0.6: patches/astro@7.0.6.patch\n```\n\nCommit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.\n\nThis is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.\n\n---\n\n## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)\n\nIf the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, titled fences like `:::warning[Heads up]`, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.\n\nThe fix is a committed **pnpm patch** (`patches/oxfmt@0.71.0.patch`), shipped with this skill at `assets/oxfmt@0.71.0.patch` — the same diff Blume's own repo applies, covering titled fences as well as bare ones. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:\n\n```bash\n# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;\n#    <skill> = this skill's directory, the one containing SKILL.md):\nmkdir -p patches\ncp \"<skill>/assets/oxfmt@0.71.0.patch\" patches/oxfmt@0.71.0.patch\n```\n\nThen register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  oxfmt@0.71.0: patches/oxfmt@0.71.0.patch\n```\n\nCommit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.\n\n- **The patch is pinned to `oxfmt@0.71.0`** (the version Blume's own repo pins). pnpm requires an exact version match, and the diff targets a file whose name is hashed per oxfmt release (`dist/markdown-*.js`), so it won't apply to any other version. If `pnpm why oxfmt` reports a different one, pin `oxfmt` to `0.71.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.59 as a peer). If the repo can't take that pin, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.\n- **The risk isn't Ultracite-only.** Any plain `oxfmt` run over Markdown (a lint-staged `oxfmt` on `**/*.md`, say) treats `:::` the same way. Check `lint-staged`/pre-commit config for globs that reach the migrated pages; `.mdx` files outside an `*.md` glob are safe.\n- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.\n\n---\n\n## Checklist for a workspace migration\n\n- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.\n- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.\n- [ ] `npmMinimalAgeGate` present? Added `blume` to `npmPreapprovedPackages` only.\n- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.\n- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22.19+ in the Vercel project.\n- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.\n- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.71.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.\n- [ ] Reported every repo-specific edit and every manual step left to the user.\n","isInternal":false,"tokens":4589,"sizeBytes":17352},{"name":"nextra.md","path":".agents/skills/blume-migrate/references/nextra.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/nextra.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Nextra → Blume\n\nNextra (on Next.js) declares navigation and per-page labels in `_meta` files — and crucially, **a folder's title and its pages' sidebar labels live in the _parent_ `_meta`**. Reconciling that cross-file inheritance is the main task. Nextra 4 was a near-rewrite, so **fingerprint the generation first** — it decides where config lives, where content lives, and what the components are called.\n\n## Detect & fingerprint the generation\n\n- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).\n- **v2/v3 (Pages Router):** content in `pages/`, `_meta.json` (v2) or `_meta.{js,ts}` (v3), a `theme.config.{js,jsx,ts,tsx}`, components from `nextra-theme-docs` (v2) or `nextra/components` (v3).\n- **v4 (App Router):** **no `theme.config`** — config lives as component props in `app/layout.{jsx,tsx}` (`<Layout>`, `<Navbar>`, `<Banner>`, `<Footer>`, `<Search>`). Content is either a **`content/`** directory (with a catch-all `app/[[...mdxPath]]/page.jsx` gateway) **or** Next page files: **`app/**/page.mdx`**.\n- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.\n\n**v4 app-directory repos need a restructure, not a `content.root`.** Pointing `content.root` at `app/` gives every route a trailing `/page` and scans `layout.jsx`/gateway files. Move each `app/foo/page.mdx` → `foo.mdx` (or `foo/index.mdx` when it has children), and exclude the non-MDX app files. The catch-all `page.jsx`, `mdx-components.*`, and `app/layout.*` are teardown artifacts — but **harvest `app/layout.*` first** (see Config).\n\n## Config\n\nNone of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:\n\n- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the footer's repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`; if its origin is not `https://github.com`, set `github.host` to that origin — with `host` omitted, Blume builds the repository and edit links against public GitHub); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer.text` (v2) / `footer.content` (v3) → `footer.copyright` when it's a copyright line: reduce the JSX to plain text, and a `{new Date().getFullYear()}` becomes `${new Date().getFullYear()}` in a template literal; anything else in it → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.\n- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>` (its children are v3's `footer.content`). Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).\n- **`next.config.*`:** harvest **`redirects()`** — static entries and `:param`/`:path*` patterns become Blume `redirects: [{ from, to }]` as written (`permanent: true` → `status: 308`, `false` → `307`); ones with regex params (`:slug(\\d+)`) or `has`/`missing` conditions move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.\n- Root `_meta` entries with **`type: \"page\"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: \"page\"` only maps at the **root**.\n\n## Navigation: `_meta` → `meta.ts` + frontmatter\n\n**Every `_meta` file must be carried over — always convert it to `meta.ts` + frontmatter, never drop it in favor of filename inference.** `_meta` is Nextra's canonical nav source (ordering, labels, folder titles); the filesystem alone can't reproduce it.\n\nFor each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):\n\n| Nextra `_meta` entry | Blume |\n| --- | --- |\n| ordinary page/folder (string or `{ title }`) | slug → parent `meta.ts` `pages` (ordering). A **page** title → that page's frontmatter `sidebar.label`. A **folder** title → that child folder's `meta.ts` `title` (title lives in the parent!). A JSX title → extract its text. |\n| `display: \"hidden\"` | frontmatter `sidebar.hidden: true` |\n| `type: \"separator\"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |\n| `type: \"menu\"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |\n| `href` (external link) | root-level → **`navigation.actions`** (`[{ label, href }]`, plain header links, matching Nextra's navbar placement) or **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route, survives on phones); only drop deep-nested ones (report) |\n| `type: \"page\"` (subfolder, not root) | drop (only root → tabs) |\n| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |\n| `theme: { layout: \"full\" }` on a page | that page's frontmatter `mode: wide` (full column width, no table of contents — the closest match; report) |\n| other `theme` keys (`sidebar: false`, `toc: false`, `typesetting`, …) | drop (report — no per-page switches for them) |\n| `\"*\"` wildcard entry | apply its value as the default for unlisted siblings, then drop |\n\nWrite each folder's `meta.ts` with its `pages` order (from its own `_meta`) and its `title` (inherited from the parent's `_meta`). A v4 **`_meta.global.*`** file holds the whole tree in one place (folder entries carry `items`) — split it into per-folder `meta.ts` files with the same rules.\n\n**Keep the order with `display: \"group\"`.** Nextra draws folders as collapsible rows and keeps pages and folders interleaved as `_meta` lists them. Blume's default `flat` display draws a folder as a plain section header, so it lists each level's pages above its subfolders — a page `_meta` puts after a folder jumps above it. Set `navigation.sidebar.display: \"group\"` (or `display: \"group\"` in the `meta.ts` of every subfolder at a level whose order must hold — one flat sibling folder reorders the level) to keep the authored order in nested folders. Groups then start closed unless they hold the current page, while Nextra opens every folder shallower than its `defaultMenuCollapseLevel` (default `2`: the top-level folders), so add `collapsed: false` to those folders' `meta.ts`. The top level (and each tab's top level) lists loose pages before folders in every display mode, so a root `_meta` that puts a page after a folder can't be matched — report it.\n\n## Frontmatter & titles — synthesize, don't just pass through\n\nMost Nextra pages have **no frontmatter**. Nextra keeps two titles apart: the **sidebar label** falls back `_meta` title → `sidebarTitle` → `title` → first body H1 → filename, while the **heading readers see** is the body H1. Blume renders frontmatter `title` as the page H1, so for **every page**: set frontmatter `title` from the body H1 (else frontmatter `title`, else the `_meta` title, else a humanized filename), **remove the body H1**, and set `sidebar.label` to the `_meta` title (else `sidebarTitle`) wherever it differs from `title`. Don't take the `_meta` label as `title` — that renames the page's heading. `asIndexPage: true` (v4 folder-index marker) → make the file the folder's `index.mdx` and drop the key. Drop any other non-schema key and report it.\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → directive. Types: `default` (the prop default) → `:::note`, `info`→`:::info`, `warning`→`:::warning`, `error`→`:::danger`, `important` (v4) → `:::note`. Bare `<Callout>` → `:::note`. Drop `emoji` (there is no `title` prop to carry — Nextra callouts have none).\n- **v2 flat names:** v2 exported `Tab` and `Card` (flat) from **`nextra-theme-docs`**; v3/v4 use `Tabs.Tab`/`Cards.Card` from `nextra/components`. Handle both: `<Cards>`/`<Cards.Card>`/`<Card>` → `<CardGroup>`/`<Card>`; `<Tabs items={[…]}>` + `<Tabs.Tab>`/`<Tab>` → `<Tabs>`/`<Tab title=\"…\">` (move labels from the parent `items` onto each `<Tab>`). `Cards` `num` → `CardGroup` `cols`.\n- **`<Steps>`:** Nextra's wraps **Markdown headings (h2–h6)**, one step per heading. Convert each heading to a `<Step title=\"…\">` child (Blume `<Steps>`/`<Step>`) and delete the heading — this also keeps step titles out of the TOC, matching Nextra's behavior.\n- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).\n- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.\n- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` pass through in `.mdx`, where Blume renders them as note/tip/warning/note/danger callouts. In a `.md` page they stay plain quotes (`BLUME_MD_GITHUB_ALERT`): rename it to `.mdx`.\n- **Strip or convert every import:** `nextra`, `nextra/*`, `nextra-theme-docs`, `nextra-theme-blog`, plus `next/image` (→ Markdown image or `<Frame>`), `next/link` (→ plain link), and local components (port or inline; report).\n\n## Code fences\n\nNextra's fence meta differs from Blume's — rewrite it: `filename=\"app.js\"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags either one left); line highlighting `{1,4-5}` carries over unchanged; **drop** word-highlight `/word/` and `copy`/`copy=false`. **Keep** inline-code `{:lang}` suffixes (`` `useState(){:js}` ``) — Blume highlights them natively. ` ```sh npm2yarn ` fences → ` ```package-install `.\n\n## Math\n\nNextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume, block math `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field). Convert Nextra's ` ```math ` fences → `$$…$$` blocks (they render as plain code blocks otherwise). Inline math is `$$…$$` too: a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence. MathJax-specific macros → report.\n\n## i18n\n\n- **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → match Blume's `dot` parser as-is, default-locale suffix included: `i18n: { defaultLocale, locales: [{ code, label }], parser: \"dot\" }`, no file moves. Page titles land in each file's own frontmatter, so they stay per-locale, but under `dot` a folder's `meta.ts` serves every locale — build it from the default locale's `_meta` and report translated folder titles from the other locales' `_meta` files.\n- **v4:** every locale has a `content/<lang>/` dir, the default included, and Nextra serves every locale under its prefix (`/en/…`). Blume's `dir` parser takes the other locales' dirs as-is, but the default locale lives **at the content root**: a `content/en/` dir is ordinary content that publishes at `/en/…` and warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`. Move the default locale's files (and `_meta`-derived `meta.ts` files) up one level, and set `i18n.hideDefaultLocalePrefix: false` to keep serving them at `/en/…` as Nextra did — or keep Blume's unprefixed default and add a `redirects` entry per page.\n\n## Package.json & teardown\n\nRepoint `dev`/`build`/`start` scripts to the Blume CLI; remove `next`/`nextra`/`nextra-theme-*` deps and add `blume`. Delete after harvesting: `next.config.*` (redirects first!), `theme.config.*` (v2/v3), `app/layout.*` + the catch-all gateway + `mdx-components.*` (v4).\n\n## Dropped — report these\n\nFooter content other than a copyright line; `primaryHue`-style theming beyond an accent color; `faviconGlyph`; `_meta` separators, menus, `newWindow`; per-page `theme` switches (and `layout: \"full\"` approximated as `mode: wide`); root-level pages `_meta` ordered after a folder; `<Bleed>`; MathJax macros; word-highlight/copy fence meta; any icon you can't reconstruct as a Lucide name.\n","isInternal":false,"tokens":3378,"sizeBytes":12135},{"name":"readme.md","path":".agents/skills/blume-migrate/references/readme.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/readme.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# ReadMe → Blume\n\nReadMe (readme.com developer hubs) is a hosted platform. Only content reaches Git: settings, the landing page, the glossary, variables, redirects, and every metrics and login feature live in its dashboard. Pages are MDX, or ReadMe's looser \"MDXish\" dialect, saved as `.md`. Published URLs are flat per section (`/docs/<slug>`, `/reference/<slug>`, `/recipes/<slug>`, `/page/<slug>`, `/changelog/<slug>`) however deep a page sits in the sidebar. The plan below keeps every one of them: section folders stay at the top of the content root, categories become `(group)` folders, and nested pages pin their flat URL with `slug`. The API reference is the exception: Blume generates operation pages at new routes, so each old one gets a redirect.\n\nTwo things go wrong with a green build and no warning: emoji blockquote callouts (plain blockquotes) and `:emoji:` shortcodes (literal text). Two more only warn: `doc:`/`ref:` links (dead links; `blume validate` reports `BLUME_UNSUPPORTED_LINK_SCHEME`) and inline `onClick` handlers (they render and do nothing; `BLUME_MDX_EVENT_HANDLER`). The codemod handles the first three; the handlers are yours.\n\n## Detect\n\n| Shape | Tells | The sidebar lives in |\n| --- | --- | --- |\n| **A. Bi-directional Git sync** (ReadMe Refactored) | `_order.yaml` in `docs/`, `reference/`, `recipes/`, `custom_pages/` and their folders; category folders with spaces (`docs/Getting Started/`); `reference/*.json` specs beside endpoint pages with `api: { file, operationId }` frontmatter; `reference/ReadMeConfig/`; `custom_blocks/`; a `main` branch holding only `changelogs/`; branches named after versions (`v3.0`) | folders and `_order.yaml` |\n| **B. `rdme@10` upload** (one-way) | CI running `readmeio/rdme@v10` or `npx rdme@10` with `docs upload`, `reference upload`, `changelog upload`, `custompages upload`, `openapi upload`; frontmatter `category: { uri }`, `parent: { uri }`, `position`, `privacy: { view }`, `content: { excerpt }` (also `rdme docs export` output) | frontmatter |\n| **C. Legacy `rdme` ≤ 9** | CI running `readmeio/rdme@v9` or older (`rdme docs ./dir`, `rdme openapi`, `rdme changelogs`); frontmatter `category: <24-hex id>` or `categorySlug`, `parentDoc` or `parentDocSlug`, `order`, `hidden`, `type: basic \\| link \\| error` | frontmatter, keyed by opaque IDs |\n\nrdme ignores folder layout, so in shapes B and C the folders say nothing about the sidebar. rdme 10 still accepts `rdme changelogs` as a hidden alias, so tell B from C by the rdme version and the frontmatter. A shape A repo can still run `rdme openapi upload` in CI: it's shape A.\n\n**Get everything before you convert.**\n\n- **Shape A** is complete per version branch. Find the branch holding the hub's default version (it can be `v3.0`, not `main`). **Changelogs live only on `main`**: `git archive <main-sha> changelogs | tar -x -C <work-dir>`. Ignore `<version>_<name>` branches (ReadMe's editing branches, kept after they merge).\n- **Shapes B and C** hold only what CI uploads, and drift: editor changes never come back, and a failing upload step stops changes going out. Inventory the live hub, export what the repo lacks, and diff pages against their live source.\n- **Exports.** A Refactored project: connect bi-directional sync to a new, empty repository (Settings → Git Connection), which writes shape A with every version as a branch; a ZIP from the Branch menu (no images); or `npx rdme@10 docs export <dir> --branch <version>` and `rdme reference export <dir>` (shape B frontmatter). A legacy project: `rdme@9` can't export; read pages through API v1 (`GET /api/v1/categories/{slug}/docs`, `GET /api/v1/docs/{slug}`) or upgrade the project. Refactored hubs usually serve a page's Markdown at `<url>.md` (docs.readme.com does), and `/llms.txt`, when it's on, lists every public page or links per-section files that do.\n\n**Go easy on the live hub.** A ReadMe hub can start answering `429` after as few as eight requests in a few minutes, even spaced seconds apart. Spend them on one page of the hub (save it: it carries the settings JSON below), `sitemap.xml`, `llms.txt`, and a few spot checks. The `files.readme.io` image CDN is separate: one run fetched 84 images there at one every half second without a `429`.\n\n## Run the codemod first\n\nFor shape A, on a branch whose name matches no ReadMe version (see Teardown), with the changelogs copied from `main` into the work tree and one page of the hub saved as `hub.html` (skip the `curl` when the user already saved one at the root):\n\n```bash\ncurl -s https://docs.example.com/docs/<any-page> -o hub.html   # settings JSON + glossary\nnode <skill>/scripts/readme-codemod.mjs --site https://docs.example.com --glossary hub.html .          # report only\nnode <skill>/scripts/readme-codemod.mjs --write --site https://docs.example.com --glossary hub.html .  # apply\n```\n\n`--site` (repeatable: add the `*.readme.io` origin too) makes absolute links to the hub root-relative. It's zero-dependency, deterministic, and idempotent (a rerun reports `Nothing to change.`). It:\n\n- moves `custom_pages/` → `page/`, `changelogs/` → `changelog/`, categories → `(Category)` group folders, `.md` → `.mdx`;\n- maps frontmatter (Frontmatter, below) and pins `slug` wherever Blume's route would differ from ReadMe's URL;\n- converts callouts, links, magic blocks, ReadMe components, variables, reusable content, icons, shortcodes, code fences, and headings (Content);\n- writes `_order.yaml` → `meta.ts`, deletes endpoint, tag, and empty parent pages, and turns link pages into redirects (Navigation);\n- writes `reference/overlays/<spec>.overlay.json` (API reference), `readme-redirects.json` (the redirects it can compute), and `readme-migration.json` (every old URL, every deleted endpoint, for the second pass).\n\n**Its report is the to-do list**: per file, what needs judgment (custom components, inline components, `<style>`, event handlers, dead links, unmapped icons), then \"Left for you\". Work through it with the sections below. A second pass after the first build handles endpoint redirects and order (API reference).\n\nShapes B and C: rebuild the tree first (Navigation, shapes B and C), then run the codemod. With no `_order.yaml` it converts page content and frontmatter only: no moves, slug pins, URL inventory, endpoint handling, or redirects (it reports link pages instead).\n\n## Config: the dashboard → `blume.config.ts`\n\nThere's no config file in Git. Every page of a Refactored hub embeds the project's settings as JSON: read them from the saved `hub.html` rather than asking. Map what's set:\n\n| ReadMe (settings JSON) | Blume | Notes |\n| --- | --- | --- |\n| `name` | `title` |  |\n| `logo`, `logo_white_use` | `logo` | download into `public/`. With `logo_white_use: true` the hub showed a white logo on a colored header, invisible in Blume's light header: recolor the white parts to `currentColor`. A bare mark → the string form (`logo: \"/logo.svg\"`); a wordmark → `{ image: \"/logo.svg\", text: \"\" }` (SKILL.md) |\n| `favicon` | `public/favicon.png` (or `.svg`/`.ico`) | picked up by name |\n| `brand.link_color` (`colors.body_highlight`) | `theme.accent` | `_dark` variants → `{ light, dark }` |\n| `brand.primary_color` (`colors.main`) | `theme.action`, or `theme.accent` when no link color | the colored header has no equivalent: report |\n| `brand.theme` | `theme.mode` |  |\n| `typography.headline` / `body` (`Open+Sans:400:sans-serif`) | `theme.fonts.display` / `body` (`{ name: \"Open Sans\" }`) | Typekit → local `variants`, or drop |\n| `nav_names` + enabled `modules` | `navigation.tabs` | one tab per enabled section with content (`{ label: \"Developer hub\", path: \"/docs\" }`) |\n| header links (`link_url` entries) | `navigation.actions` | a sign-up or \"Get API keys\" button → `navigation.cta`; a login link stays in `actions` |\n| `first_page` | `index.mdx` at the root, or a `/` redirect (status 302) | the landing page doesn't sync |\n| `custom_domain` | `deployment.site` | unless the host is Vercel or Netlify (SKILL.md) |\n| Enterprise group path (`/<project>/docs/…`) | `basePath: \"/<project>\"` | one Blume site per child project |\n| `integrations.google.analytics`, `segment`, `heap`; custom JS | `googleAnalytics()`, `segment()`, `heap()`, or `script({ src \\| content })` in `analytics` | these load only in production builds, and wait for `consent` when it's set |\n| `integrations.intercom`, `zendesk` (a support widget readers click) | a `PageFooter` component (`defineComponents`, SKILL.md \"analytics\") that loads it | not `analytics`, which can hold it until consent. Check region settings (an AU Intercom workspace needs `api_base`) |\n| `integrations.google.site_verification`, `<meta>` tags in head HTML | `seo.metatags` | other head HTML: report |\n| `stylesheet_hub2` (custom CSS) | a root `theme.css` | rules for ReadMe's classes (`.rm-*`, `.callout_*`, hashed names) match nothing: carry only rules for the site's own markup |\n| Footer HTML | `footer: { copyright, links }` | a copyright line → `copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`). Report the rest |\n| `variables.defaults` | `variables` | Content. **Defaults can be real credentials** (an API key): never copy one into config |\n| `glossaryTerms` | `--glossary hub.html` | the codemod writes `<Tooltip>`s |\n| `redirects` | `redirects` | URLs and redirects |\n| `error404` | `pages/404.astro` or Blume's default | URLs and redirects |\n| Ask AI, MCP server | `ai.assistant`, `agents.mcp` | need a server-output host adapter: report |\n\nThen the layout. **Leave the section folders where they are.** Set `content.root: \".\"` and scope `include` (`references/monorepo.md` §1), import the codemod's redirects, and add the reference:\n\n```ts\nimport { defineConfig } from \"blume\";\nimport { openapi } from \"blume/reference\";\nimport redirects from \"./readme-redirects.json\";\n\nexport default defineConfig({\n  content: {\n    root: \".\",\n    include: [\n      \"index.mdx\",\n      \"docs/**/*.mdx\",\n      \"reference/**/*.mdx\",\n      \"recipes/**/*.mdx\",\n      \"page/**/*.mdx\",\n      \"changelog/**/*.mdx\",\n    ],\n  },\n  redirects: [...redirects /* , your own */],\n  reference: [openapi({ sources: [/* API reference */] })],\n});\n```\n\n`page/` is ReadMe's custom pages at `/page/<slug>`; Blume's own `pages/` folder is for `.astro` pages, a different thing.\n\n## Navigation (shape A)\n\nWhat the codemod does, so you can check it:\n\n- **Categories → group folders.** `docs/Getting Started/` → `docs/(Getting Started)/`: the group keeps the label and adds no URL segment.\n- **`_order.yaml` → `meta.ts` `pages`**, entries as written (`pages` ignores the parentheses), less duplicates and entries with no file. A category folder's `meta.ts` also sets `title` where Blume would relabel the folder name (`Self-Serve` would show as \"Self Serve\"). A parent page's folder gets `title` from the parent and `display: \"group\"`.\n- **`slug` pins.** ReadMe serves every child flat (`/docs/<child>`; the nested path 404s), so every page below a parent folder gets `slug: docs/<stem>`, and a nested parent's own `index.mdx` takes `docs/<folder>`. A parent folder directly in a category needs none: its folder name is the route. Any stem starting with a digit gets one too, since Blume strips `^\\d+[-_.]` as an ordering prefix (`1-setup` → `/docs/setup`); dates (`12-05-2022`, `2024-01`, `2022-05-12`) and versions (`1.2`) stay whole, so their pins are harmless.\n- **Hidden pages** (`hidden: true`) were left out of the sidebar and search but served by link: `hidden: true` + `search.exclude: true`, URL kept.\n- **Empty parent pages** (frontmatter, no body) redirected to their first child on ReadMe. The codemod deletes them and adds a 302 to the first child; prefer that to keeping one as a `directory: \"card\"` landing page. A parent that stays (with a body, or kept as a landing page) whose child has the same title fails `validate --strict` (`BLUME_NAV_DUPLICATE_LABEL`): retitle one, and report it.\n- **Link pages** (`link: { url }`): deleted, with a 302 to the URL. If the link must stay visible, add it to `navigation.featured`.\n- **Section roots.** `/docs` and `/reference` redirect to the section's first page, as on ReadMe; a typed URL would 404 without it. ReadMe answers them with a 301; the codemod writes a 302, since the first page can change.\n- **Custom pages** had no sidebar: `hidden: true` plus `mode: center` (`fullscreen: true` → `mode: custom`).\n\nLeft for you: an HTML custom page (`custom_pages/<slug>.html`) → `page/<slug>.mdx` with its HTML as JSX, or `pages/page/<slug>.astro` on `PageLayout` for a full document with scripts (`docs/advanced/custom-pages.mdx`); and `/` (see `first_page`).\n\n### Versions\n\nVersion branches hold full copies of guides, recipes, and the reference; ReadMe serves the default version without a prefix and others at `/<version>/docs/<slug>`. **Recommend migrating the default version only**, with `{ from: \"/<version>/:path*\", to: \"/:path*\" }` for each other version and the default's own prefix (`/v3.0/docs/x` redirected to `/docs/x` on ReadMe). Pages that only exist in an old version then 404: report it. To keep old versions, run the codemod on each branch's checkout and copy its `docs/`, `recipes/`, and `reference/` into `<version>/` (`v2.1/docs/…` serves `/v2.1/docs/…`), drop that version's prefix redirect (a pattern over pages fails the build), add `\"<version>/**/*.mdx\"` to `content.include` (a scoped `include` never scans the copy otherwise), and set `versions.archived` (newest first) and `versions.current`. Each id is the URL segment (`v2.1`): ids must start with a letter. References, the changelog, and custom pages stay unversioned, and an archived tree's sidebar isn't scoped by tabs: report both.\n\n## Navigation (shapes B and C)\n\nRebuild the tree from frontmatter into shape A's layout, then run the codemod:\n\n- **Category** → `docs/(<Category title>)/`. Shape B's `category.uri` ends in the title (`/branches/3.0/categories/guides/Getting%20Started`): decode it. Shape C's `category` is an ID: resolve it with `GET /api/v1/categories`, the hub's sidebar, or the user. `categorySlug` names it.\n- **Parent** (`parent.uri`, `parentDoc`, `parentDocSlug`) → a folder named for the parent's slug, holding the parent as `index.mdx`; pin each child's `slug` as in shape A.\n- **Order** (`position`, `order`) → the folder's `meta.ts` `pages`.\n- **Slug**: frontmatter `slug`, else the file name. Name each file after its slug, and pin any that starts with a digit.\n- **Custom pages and changelog entries** → `page/<slug>.md` and `changelog/<slug>.md`, whatever folder CI uploaded them from: the codemod maps their frontmatter by those folder names (`custom_pages/` and `changelogs/` work too), but outside shape A it moves nothing, so the folder is the URL.\n\nThe codemod reports these navigation keys and leaves them: delete them once the tree is built.\n\n## Content\n\n**Every page becomes `.mdx`**: ReadMe's `.md` files are MDX, and in a Blume `.md` page directives stay literal text and components don't render. The codemod converts the mechanical syntax (table below). It also clears what ReadMe's lenient MDXish engine (`\"mdxish\": true` in the settings JSON) accepts and strict MDX rejects: it deletes `<!-- -->` comments, self-closes void tags, and converts `[block:…]` JSON. **It leaves everything that needs judgment, and reports it per file:**\n\n- **Inline components.** A page that defines a component (`export const Name = …`) is left byte for byte. Without hooks it renders as static HTML. With hooks it fails the build: move it to `islands/<Name>.tsx` (`export default`). One defined on several pages goes in `islands/` once.\n- **Custom components** (`custom_blocks/<Name>.mdx`): use a Blume component when it's presentational (a card grid → `CardGroup`), else move it to `islands/<Name>.tsx` and keep `<Name />` in the pages. Blume compiles Tailwind v4 utilities from the project's `.mdx` and `.tsx` files (Tailwind's default theme plus Blume's tokens, `dark:` keyed to Blume's theme), so utility classes keep working; ReadMe's CSS variables (`var(--color-border-default)`) don't exist in Blume: swap them for Blume's (`var(--blume-border)`) or its token utilities (`border-border`, `bg-muted`). Text moved from a prop into children needs `{` and `}` escaped (`\\{`), or the build fails (`BLUME_MDX_UNDEFINED_NAME`, at the brace's line).\n- **Event handlers.** `<button onClick={…}>`, `onMouseEnter`, and friends on inline HTML build green and do nothing; `blume check` and `build` warn `BLUME_MDX_EVENT_HANDLER` at each one. Rebuild the widget as an island (`grep -rnE 'on[A-Z][a-zA-Z]*=' --include='*.mdx'` outside code finds them before a build). Hubs often paste the same widget (a \"contact support\" box) into dozens of pages: one island, and replace every copy. A handler that calls a support widget's global (`Intercom('show')`) also needs that widget loaded (Config, `integrations.intercom`).\n- **`<HTMLBlock>` with `<style>`, `<script>`, or handlers** stays wrapped. **Don't move its `<style>` rules into `theme.css`**: ReadMe authors write global selectors (`p`, `ul`, `h3, h4`), which there restyle the whole site, sidebar included. Rebuild the markup with Blume components (`Tabs`, `Columns`, `CardGroup`, `Panel`, callouts). ReadMe stripped `<script>` unless `runScripts` was on: rebuild what a script did as an island or a `script()` adapter.\n- **Font Awesome `<i>` icons** in raw HTML (`<i className=\"fa fa-plug\" />`): `<Icon icon=\"<lucide>\" />`; Blume loads no Font Awesome, so the `<i>` renders empty. Tailwind classes in raw HTML (`className=\"mt-4 p-4 bg-gray-50\"`) still compile, but fixed colors ignore dark mode: prefer a component (`Panel`, a callout) or Blume's token utilities (`bg-muted`, `border-border`).\n- **Unknown components** (`<PostmanRunButton>`, `<MCPIntro>`, `<TableOfContents>`): drop or replace; each fails the build.\n- **Variables.** `<<name>>`, `{user.name}`, and `<Variable name>` became `{{name}}`; define each in `variables` (from `variables.defaults`). ReadMe filled them per reader; Blume shows one default to everyone (report). The codemod leaves `<<name>>` inside code as written and reports it: change one to `{{name}}` by hand where it should show the default (Blume substitutes in code too).\n- **Glossary terms** with no definition: pass `--glossary`, or write the `<Tooltip>` by hand.\n- **Dead links.** Hubs collect links to slugs from older versions that 404 on the live site too. Check a few live first (ReadMe cross-redirects some, `/reference/x` → `/docs/x`), then point each at the closest current page, or unlink it and keep the text. Report the list.\n- **Images** stay on `files.readme.io`: download them into `public/` and rewrite the URLs (`grep -rhoE 'https://files\\.readme\\.io/[^)\" ]+'`). Some have no extension: add one. Review every alt the codemod derived from a file name.\n- **Tables** with lists or blocks in their cells, or spanning cells (`colSpan`, `rowSpan`), stay `<table>`: check they render.\n- **Recipes**: the codemod deletes the line-highlight comments and demotes the step headings; the walkthrough modal is dropped (report). Repeat a highlighted excerpt under its step with `{1-4}` where it mattered.\n\n| ReadMe | Blume (codemod) |\n| --- | --- |\n| `> 📘 Title` / `>` / `> Body` (emoji first) | `:::info[Title]` … `:::`; 📘 ℹ️ → `info`, 👍 ✅ → `success`, 🚧 ⚠️ → `warning`, ❗️ 🛑 ‼️ ⁉️ → `danger`, any other emoji → `note`. An emoji alone means no title; `#` marks drop |\n| `<Callout icon theme>` | the directive for `theme` (`okay` → success, `warn` → warning, `error` → danger, `default` → note), else for `icon`; a first-child heading becomes the title |\n| `[block:callout]`, `[block:code]`, `[block:image]`, `[block:table]`/`parameters`, `[block:api-header]`, `[block:embed]`, `[block:html]` | the same conversions as their component forms |\n| adjacent fences (ReadMe's code tabs) | `<CodeGroup>`; untitled tabs labeled with the language, as ReadMe showed them |\n| ` ```javascript Node SDK ` | ` ```javascript title=\"Node SDK\" `; `node` → `js`, `curl` → `bash`, `cplusplus` → `cpp`, `objectivec` → `objc` |\n| `<Image src alt caption>`, `<Image …>caption</Image>`, `![1544](url \"image.png\")` | `![alt](src)`, in `<Frame caption>` when captioned; numeric or missing alts derived from the file name; `align`/`width`/`border` drop |\n| `<Embed>` (YouTube, including `google.com/sorry` interstitials), `[x](url \"@embed\")` | `<YouTube id>`; anything else → a link |\n| `<HTMLBlock>{`…`}</HTMLBlock>` | its HTML, inline: comments removed, void tags closed, braces as entities |\n| `<Table>` | a Markdown table: a cell's paragraphs joined with `<br />`, all-empty rows dropped |\n| `<Cards columns>` / `<Card iconColor badge target>` | `<CardGroup cols>` / `<Card color>`, `badge` → `<Badge>`, `target` drops; braces from props escaped |\n| `<Columns layout>`, `<Tab icon>` | `<Columns cols>`, Lucide icons |\n| `<Accordion title>` | a run → one `<Accordion>` of `<AccordionItem>`s; one alone → `<Expandable>`, or a one-item `<Accordion>` when it has an icon (`Expandable` takes none) |\n| `<Glossary>`, `<<glossary:x>>` | `<Tooltip tip>` |\n| `<Anchor>`, `<Recipe>`, `[block:recipe]`, `[block:tutorial-tile]` | a link, `<Card href=\"/recipes/<slug>\">` |\n| `custom_blocks/<Name>.md` used as `<Name />` | `_snippets/<name>.mdx` + `<include>/_snippets/<name>.mdx</include>` |\n| `doc:x`, `ref:x`, `page:x`, `changelog:x`, `blog:x`; `https://<site>/docs/x` | `/docs/x`, `/reference/x`, …; root-relative |\n| Font Awesome (`fad fa-hand-wave`) on frontmatter, cards, tabs, accordions | Lucide, or dropped and reported |\n| `:tada:` | 🎉 (unknown names reported) |\n| a body `# H1` repeating the title; any other body H1 | deleted; every body heading demoted one level |\n\nCallout titles had anchor ids on ReadMe; Blume's don't. Headings otherwise keep their anchors: both slug with github-slugger, so skip `pin-heading-ids.mjs`, which would also crawl the rate-limited hub. One exception: ReadMe's slugger also counted callout titles and the H1 the codemod deletes, so a later heading with the same text was `<slug>-1` on ReadMe and is `<slug>` in Blume. Another: for a heading that ends in an emoji or symbol after a space, github-slugger keeps a trailing dash (`## Features ✨` → `#features-`), which Blume drops (`#features`). `blume validate --strict` flags links to the old id; retarget them.\n\n## Frontmatter\n\nBlume's page schema is strict. The codemod maps:\n\n| ReadMe | Blume |\n| --- | --- |\n| `title` | `title` |\n| `excerpt` (A, C), `content.excerpt` (B) | `description` |\n| `hidden: true` (A), `privacy.view: anyone_with_link` (B) | `hidden: true` + `search.exclude: true` |\n| `hidden: true` (C) | the same, reported: rdme maps it to `privacy.view: anyone_with_link`, but check the live URL and use `draft: true` if it 404s |\n| `deprecated`, `state: deprecated` (B) | `deprecated: true` |\n| `icon`, `appearance.icon` (B) | `icon` (Lucide) |\n| `metadata.title` / `description` / `image` | `seo.title` / `description` / `image`; B's `image: { uri }` is an image id, not a URL: reported |\n| `metadata.keywords` | `search.keywords` |\n| `metadata.robots: noindex`, `allow_crawlers: disabled` (B) | `noindex: true` |\n| `next.pages`, `content.next` (B) | `related`, dropping entries that aren't pages in the repo; Blume takes 10 at most (the rest reported) |\n| `link`, `content.link` (B) | a redirect (Navigation) |\n| `table_of_contents: false` | `mode: wide` (an approximation: report) |\n| `fullscreen` (custom pages) | `mode: custom`, else `mode: center` |\n| `recipe`, `x-import`, `name`, `api`, `api_config`, `recipes` | dropped (`recipes` reported) |\n\nIt keeps keys it doesn't know and reports them: Blume rejects unknown keys, so map or delete each. It drops C's `type` (`basic`, `link`, `error`), since Blume's own `type` sets the content type. Changelog keys are under Changelog.\n\n## API reference\n\nEvery endpoint page is generated from an OpenAPI file. The codemod records each one's old URL, operation, and prose in `readme-migration.json`, and deletes it.\n\n1. **Specs → sources.** One `openapi()` source per spec in `reference/`, labeled with the ReadMe category its endpoints sat under, or the spec's `info.title`: `{ label: \"Connect\", spec: \"./reference/connect.json\" }` serves `/reference/connect`, with operations at `/reference/connect/<tag>/<operation>`. **A category that held several specs** (the report lists them) can't share a label: they'd share a route and the first would win. Give each its own label, or merge the specs first, and report the split. A single spec → `openapi({ spec })`. Add a `navigation.tabs` entry for `/reference`. A YAML spec: convert it to JSON (`npx @redocly/cli bundle x.yaml --output x.json`) and rerun the codemod, which reads JSON only.\n2. **Overlays.** `reference/overlays/<spec>.overlay.json` sets each operation's `description` to its spec text plus the prose its ReadMe endpoint page showed, and converts the ReadMe Markdown inside spec descriptions (emoji callouts, shortcodes, `doc:` links), which ReadMe rendered and Blume would show plain. Add each to its source: `{ label, spec, overlays: [\"./reference/overlays/connect.overlay.json\"] }`. JSX doesn't render in descriptions (`<` and `{` are escaped): rewrite any the report names as Markdown.\n3. **Build, then run the second pass.** List the generated routes, then hand them to the codemod with the same `--spec` pairs (the source's route, then its spec):\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist --spec /reference/connect=reference/connect.json > routes.json\n   node <skill>/scripts/readme-codemod.mjs --routes routes.json --spec /reference/connect=reference/connect.json --write .\n   ```\n\n   It adds a 301 from each old endpoint URL (`/reference/getaccount`) to its operation page, and a 302 from each tag page (`/reference/accounts-1`) to the tag's first endpoint, as ReadMe answered. It skips an old URL that is now a page (`/reference/enrich`, ReadMe's operation, against an Enrich source's overview): a redirect there would hide the page (`BLUME_REDIRECT_MATCHES_PAGE`). It reports each, so you can pick another `route` instead. It adds a 302 from `/reference` to the first endpoint when nothing else claims it, and drops any pass-one redirect that would now hide a reference page. It also rewrites links to old endpoint and tag URLs outside fenced code, dropping anchors (reported), and writes `<route>/<tag-slug>/meta.ts` (`defineMeta({ title, order, pages })`) with ReadMe's endpoint order, where it can differ from the spec's order, which the generated sidebar follows.\n\n4. **Source order, by hand.** Write `<route>/meta.ts` → `defineMeta({ order: <n> })` for each source, with the order the report gives (ReadMe's category order). It merges over the generated group's settings, so the source's `label` stays its title.\n\n- **Conceptual pages** (no `api` key) stay in `reference/(<Category>)/` at `/reference/<slug>` and join the reference tab's sidebar; their group folder's `meta.ts` takes an `order` among the sources.\n- **`reference/ReadMeConfig/`** (`api_config` pages: getting started, authentication, my requests) showed the reader's own API key and request log. The codemod keeps any prose as a page and deletes empty ones: redirect each deleted URL to the closest page.\n- **Hidden endpoints**: remove the operation with an overlay action (`remove: true`) if it should stay unpublished. `x-internal: true` likewise: Blume ignores the flag.\n- **`x-readme` extensions** (and flat spellings like `x-samples-languages`, at the root or on an operation): `samples-languages` → `codeSamples`: `shell`, `node`, `javascript`, `python`, `ruby`, `go`, `php`, `java`, `csharp`, `kotlin`, `swift`, `c`, `cplusplus` (read as `cpp`), and `powershell` carry over as written (so does `curl`, which some specs use); `objectivec`, `http`, `json`, `clojure`, `ocaml`, and `r` have no Blume generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: leave them out and report them. `code-samples` → `x-codeSamples` by overlay (`language` → `lang`, `code` → `source`, `name` → `label`). `explorer-enabled: false` → `playground: false`; `proxy-enabled` (on by default: Try It went through ReadMe's CORS proxy) → `playground: { proxy: true }` on a server-output host, or the API must allow the docs origin (say which). `codeSamples` and `playground` are per `openapi()` adapter, not per source or operation: when specs or operations disagree, take the setting most of them use and report the rest. `samples-enabled` is no longer supported on ReadMe (it hid nothing): ignore it. `headers`, `x-default`, `oauth-options`, `parameter-ordering`, `metrics-enabled`: no equivalent (report).\n- **Versioned references**: unversioned in Blume; an archived version's spec can be its own source with `route: \"/<version>/reference\"` (report).\n\n## Changelog\n\nThe codemod moves `changelogs/<slug>.md` → `changelog/<slug>.mdx` (same URL; digit-led stems pinned with `slug`, which date names no longer need) and adds `type: changelog`:\n\n| ReadMe | Blume |\n| --- | --- |\n| `date`, `published_at` (what synced repos contain), `created_at` (B) | `date` (the day). None (common in C) → reported: take it from the live entry |\n| `author` | `authors`; B's ID object drops |\n| `type` (`added`, `fixed`, `improved`, `deprecated`, `removed`; `none`) | `changelog.category`, capitalized; `none` omitted |\n| `hidden: true`, `privacy.view: anyone_with_link` | `hidden: true`: still built, out of the index and feed |\n\nIt also adds `/changelog.rss` → `/changelog/rss.xml`; the feed needs `deployment.site` or a detected host. Add a `{ label, path: \"/changelog\" }` tab. If the project is open source on GitHub and its releases are the product changelog, offer `githubReleases()` (SKILL.md \"Changelogs\").\n\n## URLs and redirects\n\n**The old URL list**: `readme-migration.json` lists every URL the repo served (hidden pages included) with its kind; add `sitemap.xml` (it omits hidden pages and empty parents), `llms.txt`, other versions (`/<version>/docs/…`), and Enterprise or translated segments (`/<project>/`, `/lang-de/`). At the end every entry must be a page or a redirect to one.\n\n- **Status codes.** Use 302 where ReadMe answered with a 302 (empty parents, tag pages, `/`), for link pages, and for section roots (ReadMe's 301 points at whichever page is first, which can change), and the default 301 for moves (operations). The codemod does, except `/`, which it leaves to you (`first_page`).\n- **ReadMe's own redirects** (`redirects` in the settings JSON, or Settings → Errors & Redirects: `old -> new` per line, full-match regex with `$1`, applied only when no page matches): a literal rule → `{ from, to }`; one-segment or rest-of-path patterns → `:name` / `:name*` (`/old(/\\S*)? -> /new$1` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`, which also sends `/old` to `/new`); anything else → host rules (report).\n- **Versions**: `{ from: \"/<version>/:path*\", to: \"/:path*\" }` (Versions).\n- **The custom 404** (`error404`): rebuild that custom page as `pages/404.astro`, or keep Blume's default. Keep or redirect its `/page/<slug>` URL.\n- **`blume validate`** accepts a redirect `from` as a link target, so rewrite links before trusting it. It warns about a redirect whose `to` leads nowhere (`BLUME_BROKEN_REDIRECT`), but not about loops or chains: run `blume audit --only redirects` after each build. A link to `/llms.txt` or another file Blume generates passes while the config has that feature on.\n- **Check every old URL** against `dist/` and `dist/blume-redirects.json` (no host adapter), following redirects until each lands on a page, or request each from `blume dev` or `blume preview` with `curl -sL`: both answer every redirect, patterns included.\n\n## Teardown\n\n**Disconnect the sync first.** With bi-directional sync on, ReadMe's GitHub app reads and writes every branch named after a version, so restructuring one rewrites the live hub. Migrate on a branch that matches no version (`blume-migration`; `<version>_<name>` would sync), and have the user disconnect the repository (Settings → Git Connection) before merging. Remove every `rdme … upload` step from CI in any shape (and an `rdme` dependency); `rdme openapi validate` can stay as a spec linter.\n\nDelete `custom_blocks/` once converted, `reference/ReadMeConfig/` leftovers, `frontmatter.schema.json`, `hub.html`, `routes.json`, and `readme-migration.json` once the redirects are final (keep `readme-redirects.json`: the config imports it). Scaffold a `package.json` with `blume` and `dev`/`build`/`preview` scripts. Moving the custom domain's CNAME is the user's step: list it.\n\n## Dropped — report these\n\n- **Hosted features**: the editor, Branches and Reviews, the AI Linter and Agent, suggested edits (Blume's edit link comes from `github`), discussions and the dev FAQ, API Metrics, My Developers, My Requests and API logs, the Developer Dashboard, health status updates, and the landing page builder (rebuild as `index.mdx` with `mode: custom`, or `pages/index.astro`).\n- **Personalized docs**: per-reader variables, API keys in Try It, `x-default` credentials, and the `ReadMeConfig` widgets. Blume's Try It takes credentials the reader types.\n- **Login-gated docs** (custom login, JWT, site password, hidden versions): Blume has no sign-in. Point at host-level protection (deployment docs, **Private docs**); it covers the whole site, so public and private pages need two sites.\n- **Changelog options** (layouts, relative dates, author toggle, translations), recipe walkthrough modals, Global Reusable Content across projects, translations through Localize or Transifex (offer `i18n` and `blume translate`), and Enterprise cross-project navigation.\n- **Approximations**: `mode: wide` for `table_of_contents: false`, image sizes and alignment, emoji and unmapped icons, unmapped `x-readme` extensions, unversioned references and changelog, custom components restyled as islands, and HTML or scripts dropped from custom blocks.\n","isInternal":false,"tokens":9064,"sizeBytes":34171},{"name":"redocly.md","path":".agents/skills/blume-migrate/references/redocly.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/redocly.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Redocly → Blume\n\nCovers **Redocly Realm** and the products on the same project format (Redoc, Revel, Reef, alone or combined), usually hosted and edited in **Reunite**. A project is a `redocly.yaml`, `.md` pages written in **Markdoc** (`{% tag %}`, not MDX), optional `sidebars.yaml` files, and API description files in the content tree, each rendered as a reference **at its own path**. The work: Markdoc tags become directives and components, each page's body H1 becomes `title`, sidebars become folders and tabs, and every API operation URL changes shape, so the old ones get generated redirects.\n\n## Detect\n\nA **Realm-family project** has at least one of these (`redocly.yaml` is optional; Realm builds a bare folder of Markdown):\n\n- a `sidebars.yaml`, or a prefixed `<name>.sidebars.yaml`, at the root or in a section folder;\n- `.md` pages using Markdoc tags (`{% admonition`, `{% tabs`, `{% partial`);\n- `@theme/`, `_partials/`, `@l10n/`, or `@<version>/` folders, or `*.page.tsx` React pages;\n- a dependency on `@redocly/realm`, `@redocly/redoc`, `@redocly/revel`, `@redocly/reef`, or a combined package (`@redocly/redoc-revel`, `@redocly/redoc-reef`, `@redocly/revel-reef`). Reunite builds projects with no `package.json`, so a missing one proves nothing.\n\nThree look-alikes:\n\n- **A lint/bundle-only `redocly.yaml`**, with none of the above and only keys Redocly CLI reads (`extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `apis`, `resolve`, `telemetry`), configures `redocly lint`/`bundle`. Leave it untouched and migrate the framework the docs actually use.\n- **The legacy Redocly Developer Portal** (Gatsby; `siteConfig.yaml`, `theme.ts`, a `@redocly/developer-portal` dependency, `.mdx` pages, `*.page.yaml` reference pages, and a `sidebars.yaml` that maps named sidebars to groups of `pages:`). Its content is MDX with `:::` admonitions, so this file's Markdoc mappings don't apply: use SKILL.md's general model, this file's Config, API reference, and URL sections, and tell the user it was the legacy portal.\n- **A standalone Redoc embed** (an HTML page loading `redoc.standalone.js` with `<redoc spec-url>` or `Redoc.init`, or a `redocly build-docs` script): migrate only the spec, with `openapi({ spec })`, and rewrite links to its `#tag/…`/`#operation/…` anchors. A Redoc-product project (`@redocly/redoc` alone) has no Markdown either.\n\n**Keep the content where it is**: `content.root: \".\"`, with `content.include` scoped to the real content folders (`[\"guides/**/*.{md,mdx}\", \"apis/**/*.{md,mdx}\"]`, per `references/monorepo.md` §1), plus each root-level page by name (`\"index.{md,mdx}\"`, `\"changelog.{md,mdx}\"`). A root `*.md` glob would also publish `README.md`, which Realm never did. Routes then keep their paths, `/`-rooted partial paths keep resolving, and the description files stay where `redocly.yaml`'s `apis.*.root` and the lint scripts expect them.\n\n**Realm publishes every `.md` in the project** except the root `README.md` and `public/`, partials folders, `static/`, `@theme/`, and the `ignore` globs, so the old sitemap often lists accidental pages: `.github/pull_request_template`, files under `.claude/`, planning notes. Leave those out of `content.include`, add no redirect for them, and list them in the report so the user can add one if they want full URL coverage. Underscore-prefixed files outside a partials folder were pages in Realm but are excluded by Blume's default `**/_*`: rename any that are real pages (`content.exclude: [\"!**/_*\"]` would publish every partial too).\n\n## Config: `redocly.yaml` → `blume.config.ts`\n\nResolve `$ref`s first (`redirects: { $ref: ./redirects.yaml }` is common). Product folders can hold their own `redocly.yaml`, and `env.<environment>` blocks override per environment: map the production values and report the rest.\n\n| Redocly | Blume | Notes |\n| --- | --- | --- |\n| `seo.title` (`seo.projectTitle` when set) | `title` | Blume appends ` - <title>` to every docs page's `<title>` (not to a page titled the same as the site). Realm added a suffix only with `projectTitle`, so on most Realm sites this is a visible change: report it |\n| `seo.description` | `description` |  |\n| `seo.siteUrl` | `deployment.site` | unless the new host is Vercel or Netlify (auto-detected). Cloudflare Pages exposes only a per-deploy URL, so keep it there |\n| `seo.meta` (`[{ name, content }]`), `seo.keywords` | `seo.metatags` (`{ [name]: content }`, `keywords: \"a, b\"`) | minus the tags Blume writes itself; the build names the setting that owns a refused tag |\n| `seo.lang` | nothing for English; else `i18n: { defaultLocale: \"<code>\", locales: [{ code: \"<code>\", label: \"<language name>\" }] }` | `label` is required |\n| `seo.image`, `seo.jsonLd`, `seo.llmstxt` | **drop** (report) | Blume generates per-page cards, JSON-LD, and `llms.txt`; a page in `llmstxt.excludeFiles` gets `ai.exclude: true` |\n| `logo.image` / `logo.srcSet` (`\"./l.svg light, ./d.svg dark\"`), `altText`, `link` | `logo` / `logo: { image: { light, dark, alt }, href }` | move the files into `public/`; a wordmark also gets `text: \"\"` |\n| `logo.favicon` | **drop the field**: copy the file to `favicon.<ext>` or `icon.<ext>` in the root or `public/` | detected by filename, for `.svg`, `.png`, and `.ico` only: convert any other format |\n| `navbar.items` | `navigation.tabs` / `actions` / `cta` | see Navigation |\n| `footer.items` (columns, each a `{ group, items }` or a single item) | `footer.links` (one row) + `footer.socials` | flatten every column's items in order (`page` → its route). Links to a platform in Blume's `footer.socials` list (`docs/configuration/index.mdx`) become socials; a podcast (Spotify, Apple Podcasts) is `podcast`; others (TikTok) stay links. `copyrightText` → `footer.copyright`. Column labels drop (report) |\n| `redirects` (`'/from': { to, type }`) | `redirects: [{ from, to, status: type }]` | write `from` without a trailing slash; a trailing `/*` carries over as written (`{ from: \"/a/*\", to: \"/b/*\" }`), and also matches the bare `/a`. Blume takes 301/302/307/308 only |\n| `markdown.toc` | `toc: { maxHeadingLevel: <depth> }`, or `toc: false` for `hide` | Realm's default depth 3 is Blume's default |\n| `markdown.lastUpdatedBlock` | `lastModified: \"git\"`, **only when declared** and not `hide: true` | Realm shows \"Last updated\" by default; if the source left it at that, offer `lastModified: \"git\"` in the report. Either way, every page the migration edits (nearly all of them, since each H1 moves) shows the migration commit's date; a rename alone keeps the page's date. Say so |\n| `markdown.editPage.baseUrl` | `github: { owner, repo, branch, dir? }` | the path after the branch → `dir`; a non-`github.com` origin → `host` |\n| `ignore` | `content.exclude: [ … ]` | the globs add to Blume's defaults (`**/_*`, `**/.*`), so `_partials/` stay out of routing |\n| `colorMode` | `theme.mode` | `ignoreDetection` with a first mode of `dark` → `\"dark\"` |\n| `banner` (a list) | `banner: { content, link: { href, text }, dismissible }` | one banner: keep the first unscheduled, untargeted one. `content` is plain text, so a Markdown link in it becomes `link`; colors, targets, schedules drop |\n| `feedback` | `hide: true` → `feedback: false`; `type: comment` → `{ comments: true }` | other types drop |\n| `search` | omit (built-in Orama); `suggestedPages` → `search: { provider: orama(), popular }` (`orama` from `blume/search`) | `engine` and `filters` (facets) drop |\n| `analytics` | `ga.trackingId` (a `G-` id) → `googleAnalytics({ id })`, `gtm.trackingId` → `googleTagManager({ id })`, `amplitude.apiKey` → `amplitude({ key })`, `segment.writeKey` → `segment({ key })`, `heap.appId` → `heap({ id })`, `adobe.scriptUrl` → `adobe({ url })`, `fullstory`/`rudderstack` → `script()` | from `blume/analytics`. A `UA-` id is Universal Analytics, dead since 2023: drop it and say so |\n| `scripts.head` / `scripts.body` | one `script({ src, strategy, attributes })` each in `analytics` | local `./static/x.js` → `/x.js`; `async`/`defer` → `strategy`. **Adapters in `analytics` load only in production builds and wait for consent when `consent` is set.** A script the page needs to work (a support widget, a form embed that buttons call) goes in a `PageFooter` layout slot component instead (`<script is:inline src=\"…\">`), which runs in `blume dev` too. That slot renders on every content page, API operation pages included, but not on `mode: custom` or `frame` pages or custom `pages/`: report where the script is now missing |\n| `links` | a Google Fonts stylesheet → `theme.fonts` (`{ body, display, mono }`) | each role a curated slug, or `{ name: \"Family\" }` for anything else; mind renamed families (Source Sans Pro is `source-sans-3`). `preconnect` links drop |\n| `access` (`requiresLogin`, `rbac`, `sso`, `idps`), root `rbac`/`requiresLogin`/`sso`, `ssoDirect`, `userMenu` | **drop** | Blume has no login: point at host-level protection (deployment docs, **Private docs**), which covers the whole site; report every protected path |\n| `l10n` (older: `i18n`) | `i18n` | see i18n |\n| `apis`, `openapi`, `graphql` | `reference` adapters; **keep `apis` in `redocly.yaml`** | see API reference |\n| `products` (`{ id: { name, folder, icon } }`) | `navigation.selectors: [{ kind: \"product\", label, items: [{ label: name, path: \"/<folder>\" }] }]` |  |\n| `aiAssistant`, `mcp` | nothing | offer `ai.assistant` / `agents.mcp` (server output) as follow-ups |\n| `mockServer`, `catalogClassic`, `entitiesCatalog`, `scorecard*`, `developerOnboarding`, `apiFunctions`, `skills`, `corsProxy`, `responseHeaders`, `reunite`, `removeAttribution`, `palette`, `breadcrumbs`, `codeSnippet`, `navigation`, `navbar.hide`, `footer.hide`, `sidebar.hide` | **drop** (report) | `consent` → a `blume/consent` adapter if the site runs one; `navigation.actions` are Blume's default page actions |\n| `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, `recheck` | **keep in `redocly.yaml`** | see Teardown |\n| `'{{ process.env.X }}'` | `process.env.X` in `blume.config.ts` |  |\n\n## Navigation: `sidebars.yaml` and `navbar` → folders and tabs\n\nWith no sidebar file, Realm generates the sidebar from the file tree (natural sort, `index` first), which Blume's generated sidebar reproduces. Otherwise every file ending in `sidebars.yaml` is a sidebar (one only pulled in through another's `$ref` isn't), `page:` paths resolve from the sidebar file (or the project root with a leading `/`), and **a page no sidebar lists shows no sidebar** (unless its frontmatter `sidebar.path` names one): in Blume's generated tree it would appear. `sidebar.hidden: true` hides it, but also drops it from search, the sitemap, and `llms.txt`, where Realm still listed it: hide only orphans nobody should find, and report which you hid. Realm labels default to the page's `seo.title`, then its first heading, so most `label`s only restate the new `title`.\n\n- **`group` + `items`** → a folder with a `meta.ts` (`title`, and `pages` in the declared order). A group whose pages sit flat beside others, or come from several folders, has no on-disk counterpart: move its pages into a subfolder (a page the group opened becomes its `index`, keeping its URL; the others get redirects) or use an explicit `navigation.sidebar`, per SKILL.md \"Config-declared nesting\".\n- **`label`** → `sidebar.label`, only where it differs from `title`. **`href`** → an explicit sidebar link or `navigation.featured`. **`directory`** → leave the folder to the generated sidebar. **`$ref`** → inline it.\n- **`expanded`**: Realm groups collapse by default, which `navigation.sidebar.display: \"group\"` reproduces. `expanded: true`/`always` → `collapsed: false` in that folder's `meta.ts`. **`menuStyle: drilldown`** → `display: \"page\"`.\n- **`separator`** and **`separatorLine`** have no equivalent: a labeled separator heading a run of pages is usually better as a group; report the rest.\n- **`badges`** → `sidebar.badge` (first name). **`icon`** → Lucide (Icons). `rbac`, `disconnect`, `additionalProps`, `external`, `*TranslationKey` drop.\n- **An API description listed as a `page`** places that API's generated tree. Blume can't put generated operation pages into an explicit `navigation.sidebar`; they sit wherever the source's `route` puts them (see API reference).\n\n**Several sidebars → header tabs.** Realm gives each page the sidebar that lists it, so one sidebar file per section (`guides/sidebars.yaml`), or top-level `tab:` entries in `sidebars.yaml`, is a tabbed site: one `navigation.tabs` entry per section folder (a `tab` with `menu:` → a tab with `items`). The root sidebar's pages, if they share no folder, get a tab with `path: \"/\"` and an `href` to their landing page; Blume picks the longest matching tab path, so section routes still select their own tab.\n\n**`navbar.items`**: an item that opens a section with its own sidebar is that section's tab. The first item into the root sidebar is the root tab; other items into the same sidebar become `navigation.actions`, or drop if they duplicate a tab's target. A \"Sign up\" button → `navigation.cta` with an absolute URL; a GitHub link → `github`.\n\n## Content: Markdoc → MDX\n\nBlume parses `.md` as plain Markdown; directives, components, and ` ```mermaid ` fences need `.mdx`. **Rename only the pages that end up with one of those** (`<include>` works in `.md` too), and leave the rest `.md`, so the diff stays small and their history keeps its file names. Then fix what MDX parses differently, in pages and in every partial an `.mdx` page includes (an included partial parses as MDX):\n\n- **Angle-bracket autolinks** (`<https://…>`, `<team@acme.com>`) fail to compile: write `[text](url)` or a bare URL.\n- A literal `{` or `}` in prose: escape it or put it in inline code. **HTML comments** → `{/* */}`.\n- **Raw HTML** must be JSX-valid: string `style=\"…\"` → `style={{ … }}` or a class in `theme.css`; close void tags (`<br />`). `<figure>` + image + `<figcaption>` reads better as `<Frame caption=\"…\">` around the image. A raw `<iframe>` of a YouTube video → `<YouTube id=\"…\" />`.\n- **File links follow the rename**: Blume resolves `./setup.md` by source file, trying the other extension when that file is gone, so a link to a page renamed `setup.mdx` still lands on it, `slug` and numeric prefix included. Update them anyway: a link to the old name breaks wherever the Markdown is read as files, such as on GitHub.\n\n**Every Markdoc tag must be converted**: in `.md` an unconverted tag ships as literal text with a green build, and `blume check` and `build` only warn `BLUME_TEMPLATE_TAG` at its line (in `.mdx` the page fails to compile, and `blume check` and `validate` report it as `BLUME_MDX_SYNTAX`). Finish with `grep -rn '{%' <content folders>` returning only samples that document Markdoc itself, in code where the warning skips them.\n\n**The body H1 is the title.** Realm renders the page's first `# H1` and uses it for `<title>`, sidebar label, and breadcrumb (unless the page sets `seo.title`; a page with no H1 uses its first heading of any level). Move it to frontmatter `title`, delete it from the body, and demote any later H1 to `##`. Links to the H1's own anchor (`#<h1-slug>`, often the first entry of a hand-written contents list) now break: remove them or point them at the page.\n\n| Markdoc | Blume (`.mdx`) |\n| --- | --- |\n| `{% admonition type=\"info\" name=\"Title\" %}` … `{% /admonition %}` | `:::info[Title]` … `:::` (`success`, `warning`, `danger` keep their names; `idea` → `tip`). No `name` → no brackets |\n| `{% tabs %}` / `{% tab label=\"X\" %}` | `<Tabs>` / `<Tab title=\"X\">`; `tabs id=\"k\"` → `param=\"k\"`. **A group of tabs that each hold only a code fence** → `<CodeGroup>` with titled fences (` ```ts title=\"X\" `); a bare fence directly inside `tabs` was a tab named by its `label` |\n| `{% code-group %}` | `<CodeGroup>` with titled fences; `mode=\"dropdown\"` → `<CodeGroup dropdown>` |\n| `{% partial file=\"…\" variables={a: \"x\"} /%}` | `<include a=\"x\">…</include>` on its own line (Partials) |\n| `{% raw-partial file=\"…\" /%}` | inline the file's text |\n| `{% code-snippet file=\"./x.js\" title=\"x.js\" /%}` | `<include lang=\"js\" meta='title=\"x.js\"'>./x.js</include>` (the file inside `content.root`). `from`/`to`/`before`/`after` ranges: paste the lines into a fence instead (report) |\n| `{% cards columns=N %}` / `{% card title to icon image %}` | `<CardGroup cols={N}>` / `<Card title href icon img>`; `layout=\"horizontal\"` → `horizontal` |\n| `{% accordion-group %}` / `{% accordion title expanded %}` | `<Accordion>` / `<AccordionItem title defaultOpen>` |\n| `{% numbered-list %}` / `{% numbered-item %}` | `<Steps>` / `<Step title>` (an item's leading heading becomes `title`) |\n| `{% img src alt caption /%}` | `![alt](src)`, inside `<Frame caption=\"…\">` when it had a caption. `srcSet` light/dark pairs have no equivalent: keep one image (report) |\n| ` ```mermaid ` fence, `{% diagram type=\"mermaid\" %}` | a ` ```mermaid ` fence (inline the file), in an `.mdx` page. PlantUML and Excalidraw have no renderer: export to SVG (report) |\n| `{% icon name=\"…\" /%}`, `{% inline-svg file /%}` | `<Icon icon=\"<lucide-name>\" />`, `![alt](./file.svg)` |\n| `{% table %}` | a pipe table (raw HTML for `colspan`/`rowspan`) |\n| `{% if %}` / `{% else /%}` | no conditionals: keep the branch the live site showed; per-reader branches (`$rbac`, `$user`) keep the public one (report) |\n| `{% $frontmatter.x %}` / `{% $env.PUBLIC_X %}` | the literal value / `{{public-x}}` with a `variables` entry (`\"public-x\": process.env.PUBLIC_X ?? \"\"`: values must be strings, so an unset variable would fail the config) |\n| `{% json-schema %}`, `{% json-example %}`, `{% openapi-code-sample %}`, `{% openapi-response-sample %}`, `{% replay-openapi %}` | a fenced sample or a `TypeTable`, or a link to the operation page, which renders samples and Try it itself (report) |\n| `{% markdoc-example %}`, `{% login-button %}`, `{% connect-mcp %}`, `{% code-walkthrough %}` | the fence only / drop (report); a walkthrough becomes prose and fences or `<Steps>` |\n| `## Heading {% #id %}` / `{% .class %}` | `## Heading [#id]` / drop |\n| any other tag | a custom tag from `@theme/markdoc/schema.ts`: rewrite its markup, or make its component an island (`islands/<Name>.tsx`, used as `<Name …/>`) |\n\n**Code fences**: Realm reads fence meta only from an annotation: ` ```js {% title=\"app.js\" highlight=\"{2,4-6}\" %} ` → ` ```js title=\"app.js\" {2,4-6} `, and `highlight=\"/word/\"` → `// [!code word:word]`. A bare word after the language was ignored by Realm but becomes a title in Blume: delete stray ones. `[!code highlight|++|--|word|focus|error|warning]` comments carry over.\n\n**Partials** live in `_partials/` folders at any depth, which Blume also keeps out of routing. `markdown.partialsFolders` replaces that default: add the folders it names to `content.exclude` (which keeps Blume's defaults) or rename them with a leading `_`. A leading `/` in `file` meant the project root, which is `content.root` when that's `\".\"`. Variables become lowercase attributes (`favFood` → `favfood`), read in the partial as `{{favfood}}`; a partial that read `$frontmatter` needs the value passed in. A partial used only by `.md` pages can stay `.md`.\n\n## Frontmatter\n\n| Redocly | Blume |\n| --- | --- |\n| body `# H1` | `title` |\n| `title` (top level) | **drop.** Realm never rendered it on Markdown pages (`<title>` was the H1), so moving it to `seo.title` would change live titles. Offer it in the report when it looks like a deliberate SEO title |\n| `description` (top level) | `description` (Realm used the site description instead; this improves the meta description) |\n| `seo.title`, `seo.description`, `seo.image` | the same keys (`seo.title` was also Realm's sidebar label: copy it to `sidebar.label` to keep that) |\n| `seo.keywords` | `search.keywords`, as a list (a string splits on commas). Blume uses them as search terms and writes no per-page `<meta name=\"keywords\">` |\n| `excludeFromSearch: true` | `search.exclude: true` + `ai.exclude: true` (+ `noindex: true` to keep it out of the sitemap, as Realm did) |\n| `slug` | `slug` (a full route); a list → the first as `slug`, the others as redirects |\n| `redirects: { '/old/': {} }` | a config `redirects` entry per path, `from` without the trailing slash. Exact entries beat wildcards, as in Realm |\n| `sidebar: { hide: true }` (hides the sidebar on this page; it doesn't take the page out of one) | `mode: center`. Not `sidebar.hidden`, which removes the page from the navigation, search, and the sitemap |\n| `sidebar: { path }` (which sidebar this page shows) | drop: the tab whose `path` holds the page decides its sidebar |\n| `navigation.nextButton` + `previousButton` `hide: true` | `pagination: false` |\n| `navbar`/`footer` hidden | `mode: custom` (the header only: no sidebar, title, or footer). The header always shows: report a hidden navbar |\n| `rbac` | drop; report the page as access-controlled |\n| `markdown.*`, `codeSnippet`, `breadcrumbs`, `colorMode`, `feedback`, `search`, `versionPicker`, `banner`, `template`, `keywords` (Typesense search curation), `seo.meta`/`jsonLd`/`lang`/`priority` | drop (report what was set) |\n| top-level `editPage` (including legacy `editPage: { disable }`) | drop without reporting: Realm reads only `markdown.editPage`, so it never had an effect |\n| custom keys (`metadata`, …) | `frontmatter.extend` if the site needs them, else drop |\n\n## API reference\n\nRealm renders every description in the content tree (a `.yaml`/`.json` with a top-level `openapi:`, `swagger:`, or `asyncapi:` key, or a `.graphql` schema) **at its path without the extension**: an overview at `/apis/orders/openapi`, a page per tag, and a page per operation below it. `apis` entries match files by `root` and carry lint and display settings; a description needs no entry to render. List every description, and report any `apis.*.root` that points at a missing file.\n\n- **Use `openapi()`** from `blume/reference`, one source per description: like Realm, it gives each operation its own page and URL. (`scalar()` would collapse each API onto one page.) AsyncAPI → `asyncapi()`; GraphQL → `graphql()` with `endpoint`. Leave the files where they are.\n- **Choose each `route`.** When the description shares its folder with guides (`apis/orders/openapi.yaml` beside `apis/orders/quick-start.md`), keep the old path (`route: \"/apis/orders/openapi\"`): its overview URL survives, and the reference sits in that folder's group beside the guides. **When the description is alone in its folder** (`apis/orders/openapi.yaml` and nothing else), the old path nests the reference's group inside a folder group that holds only it: use the folder as the route (`route: \"/apis/orders\"`), so the reference is that group. That moves one overview URL, which the catch-all below covers. Either way, `label` (the `sidebars.yaml` label, else the spec's `info.title`) names the group. Choosing a route inside a topic folder elsewhere moves the reference to where a `sidebars.yaml` had placed it, at the cost of more redirects; say which you chose. Add a `navigation.tabs` entry if an API was a top-level section.\n- **Spec problems fail `blume validate --strict`** (`BLUME_OPENAPI_*` warnings, e.g. path parameters declared `in: query` or missing). Fix them with an OpenAPI Overlay in the source's `overlays`, leaving a generated spec untouched, and report them for a fix at the source.\n- **Decorators don't run.** Realm applies `decorators` (`remove-x-internal`, `filter-out`, `info-override`), and the `preprocessors` and `plugins` behind them, when it bundles a description; Blume reads the raw file. When any are configured, point `spec` at `redocly bundle <api> --output <file>` output or express the change as an Overlay.\n- **Tag labels and order.** Blume reads `x-displayName` for each tag group's label, and orders tags as the spec's `tags` list does and operations as the spec lists them. `x-tagGroups` isn't read: when the spec has them and their flattened order (how Realm ordered tags) differs from `tags`, write `<route>/<tag-slug>/meta.ts` with `defineMeta({ order })`, the tag's position there (the folder under `content.root`, mirroring the route; it must sit in a folder `content.include` reaches, or `BLUME_META_OUTSIDE_INCLUDE` warns that it's never read). It merges over the generated title and order. Get each tag's folder name from Blume's routes (below), not by slugifying. `x-tagGroups` names drop, and tags left out of every group, which Realm hid, now appear: report both. `x-traitTag`, `x-logo`, and `x-badges` are ignored.\n- **Code samples**: `codeSamples.languages[].lang` (root and per-API `openapi:`) → `codeSamples`; Blume reads Redocly's names (`curl`, `JavaScript`, `Node.js`, `Python`, `Java`, `C#`, `PHP`, `Go`, `Ruby`). `R`, `Payload`, `Java8+Apache`, and `C#+Newtonsoft` have no generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: drop them and report. With no languages set, Realm showed its full default list and Blume shows `curl`, `js`, `python`: report the difference. `x-codeSamples` render natively. `codeSamples`, `playground`, and `expandSchemas` are `openapi()` options, shared by all its sources: APIs whose settings differ go in separate `openapi()` entries.\n- **Try it**: `hideReplay: true` → `playground: false`. Realm proxied Try it requests by default; Blume sends them from the browser. Set `playground: { proxy: \"<url>\" }` only for a proxy the user runs; **never carry over a Redocly-hosted one** (`/_api/cors/`, the default; `https://cors.redocly.com`; `https://cors.redoc.ly`). Probe each API's server for CORS with `curl -si -X OPTIONS -H 'Origin: https://<docs-host>' -H 'Access-Control-Request-Method: GET' <server-url>/<some-path>` and list the ones without a matching `access-control-allow-origin` as a follow-up: `playground: { proxy: true }` fixes them once the user picks a server-output host.\n- **Other display options**: `excludeFromSearch` → the source's `includeInSearch: false`; `schemasExpansionLevel: all` → `expandSchemas: true`; `layout`, download buttons, `showExtensions`, `feedback` (Blume's page rating shows on every overview and operation page, with no per-source switch), `mockServer`, and the rest drop (report).\n\n## URLs and redirects\n\n**Save the old URL list first**: a Realm site with `seo.siteUrl` serves `<site>/sitemap.xml`, listing every page **and every API overview, tag, `section/`, and operation page**. Every URL on it must end up a Blume page or a redirect to one.\n\n- **Page routes.** Realm slugs the file path: lowercased, spaces → `-`, `_` and `.` kept, **numeric prefixes kept**. Blume keeps the filename's case and its spaces, and strips a numeric prefix followed by `-`, `_`, or `.` (`01_setup` → `/setup`; a date like `2024-01` or a version stays whole). Rename mixed-case and spaced files to match; a numbered file needs `slug` (or a redirect) to keep its number; a nested `README.md` (Realm `/…/readme`, Blume `/…/README`) becomes `readme.md` to keep its URL.\n- **Moved pages** (materialized groups, `index` promotions, tabs) and **Realm's own redirects** (config, frontmatter `redirects`, extra `slug`s) all become `redirects`. A long generated list reads best in its own `redirects.ts` module, imported into `blume.config.ts`.\n- **Operation pages change shape.** Realm: `<old route>/<tag-slug>/<operationId lowercased>` (`getMultipleDomains` → `getmultipledomains`, `Domains.getDomain` → `domains.getdomain`), or `<old route>/<tag-slug>/paths/<JSON pointer>/<method>` with no `operationId` (`…/paths/~1pets~1%7Bid%7D/get`). Realm tag slugs keep underscores. Blume's slugs differ, so write **one exact redirect per operation**, generated:\n  1. `blume build` once with the references configured.\n  2. `node <skill>/scripts/operation-routes.mjs dist > operation-routes.json` prints, per reference route, every endpoint (`GET /pets/{id}`, or `POST petAdopted` for a webhook) mapped to its Blume route, read from the build itself (a Vercel server build's pages are in `.vercel/output/static`: pass that). `--spec <route>=<file.json>` adds operationIds (convert YAML first: `npx @redocly/cli bundle openapi.yaml --output openapi.json`).\n  3. For each old operation URL, find its operation in the spec: match the last segment against each `operationId` lowercased, or decode the pointer (`%7B` → `{`, `~1` → `/`, `~0` → `~`) to a path plus method. Look up `METHOD path` under the new route, and emit `{ from: <old URL path>, to: <route> }`.\n- **Tag, `section/`, and tag-description heading pages** (`<old route>/<tag>`, `<old route>/section/<heading>`, `<old route>/<tag>/<heading>`) don't exist in Blume: send them to the overview. When the route moved, one catch-all does it: `{ from: \"<old route>/:path*\", to: \"<new route>\" }` (exact operation entries still win). When the route stayed, use `{ from: \"<route>/section/:path*\", to: \"<route>\" }` and `{ from: \"<route>/:tag\", to: \"<route>\" }`, plus exact entries for tag headings; a pattern that covers a page fails the build (`BLUME_REDIRECT_MATCHES_PAGE`).\n- **Legacy hash links** (`#operation/<id>`, `#tag/<tag>`, `#section/…`) worked on Realm, which resolved them in the browser, and still appear **in content, partials, and the description files' own descriptions**. A server can't redirect a fragment. `blume validate` flags one on a page that still exists as a broken anchor, but accepts any redirect `from` as a link target, so it misses the ones whose path now redirects: `grep -rn '#operation/\\|#tag/' <content> <spec files>` and rewrite each to its Blume route. Rewrite absolute links to the site itself (`https://<docs-host>/…`) to root-relative paths too; `blume audit` flags the ones left.\n- **Trailing slashes.** Realm answered `/a/` with a 301 to `/a`, and so do `blume dev` and `blume preview`, for pages and redirect sources alike. In production it depends on the host: static hosts serve a page's `a/index.html` at `/a/`, and server builds (`vercel()`, `node()`) match `/a/` for a redirect `from`, but whether a static host applies a redirect rule to the slashed form varies. When old links used trailing slashes (frontmatter `redirects` often did), check one slashed redirect on a preview deployment and report it.\n- **Versions and locales**: Realm drops the `@` from version folders (`api/@v1/x.md` → `/api/v1/x`) and serves the default version without its segment; non-default locales get the lowercased code (`@l10n/es-ES/` → `/es-es/…`).\n- **Check coverage**: after the build, every old URL should be a page in `dist/` (`<path>/index.html`) or resolve through `dist/blume-redirects.json` (exact entries first, then patterns) to one. That manifest is written for a static build with no host adapter; with one, request each URL from a running `blume dev` (or `blume preview` of a static build) instead: both answer every redirect, patterns included. `blume audit --only redirects` checks the redirects themselves.\n\n## Assets, theme, React pages, and the repo\n\n- **`static/` → `public/`**: Realm serves `static/` at the root, so root-relative references keep working. Drop a Redocly-hosting `robots.txt` (`/~/`, `/cdn-cgi/` rules); Blume writes its own.\n- **`.gitignore`**: Realm projects often list `public/` (and sometimes `_partials`), which would leave moved files uncommitted: remove those lines. Add `.blume/`, `dist/`, and `.env.local`, as `blume init` does.\n- **Node**: Blume needs Node 22.19 or later, and Realm templates often pin 20 in `.nvmrc`. Update `.nvmrc`/`.node-version`, `engines.node`, and CI `node-version`.\n- **`@theme/styles.css`**: the brand color (`--color-primary-base` or the variable it points at) → `theme.accent`; check it against dark mode, which `blume audit` checks for contrast. Rules for the site's own classes (a landing page, figures) move to a root `theme.css`, with `html.dark` selectors rewritten as `[data-theme=\"dark\"]`. Rules for Realm's components don't apply: report them.\n- **`@theme/components/*`**: a component a page uses → an island (`islands/`, or imported by a custom page with `client:*`); an ejected Realm component → the matching layout slot if one exists, else drop. `@theme/layouts`, `Templates`, `plugin.*`, and `markdoc/` drop once their tags are converted.\n- **`*.page.tsx`**: `index.page.tsx` → `pages/index.astro` on `PageLayout`, with interactive parts as React islands, or an `index.mdx` with `mode: custom` when it's mostly links. Its `export const frontmatter` gives the title. `404.page.tsx` → `pages/404.astro`, or Blume's built-in 404. A React-state light/dark image swap becomes two images with CSS hiding one.\n\n## i18n\n\n`l10n: { defaultLocale, locales: [{ code, name }] }` → `i18n: { defaultLocale, locales: [{ code, label: name }] }`. Pages under `@l10n/<code>/` move to `<code>/` at the content root. Blume uses `code` as written in URLs, so write it lowercase (`es-es`, folder too) to keep Realm's URLs. `translations.yaml` label keys → per-locale `meta.ts` titles and tab label maps; UI strings → `i18n.ui` where Blume has one; report the rest.\n\n## Versions\n\nRealm versions a folder through `@<version>/` subfolders and an optional `versions.yaml` (`default`, order; with no `default`, the last listed version is the default); the default version serves without its segment. Blume's versioning is site-wide:\n\n- **Version folders at the content root** → Blume versioning: the default version becomes the current tree (`versions.current: { label }`, required), the others top-level folders listed newest first in `versions.archived` as `{ id, label? }` (ids start with a letter: `@1.0` → `v1.0`, redirect `/1.0/:path*`).\n- **A versioned section** (`api/@v1/`) → plain folders reproduce the URLs: the default version's files move up into `api/`, the others into `api/v1/` (a name like `1.0` stays whole). Add a `navigation.selectors` entry `{ kind: \"version\", … }` and report the missing old-version notice and version-scoped search. Versioned descriptions → one `openapi()` source each.\n\n## Icons\n\nRealm icons are Font Awesome names (`book`, `solid check-circle`, `brands github`) or image paths. Strip the pack prefix and map to Lucide with the table in `references/mintlify.md`; image paths work as written; brand icons other than the few Lucide ships (listed there, `github` among them) have no Lucide match (report).\n\n## Teardown — keep lint and bundle working\n\n1. **Trim `redocly.yaml`, don't delete it**: remove the portal keys and keep `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, and each `apis` entry's `root`, `output`, `overlays`, and lint keys (drop its `openapi`/`graphql`/`theme` display keys). If nothing but `apis` roots remain, keep it anyway when scripts or skills run `redocly lint`; otherwise ask. Run the lint script before and after: the result should match.\n2. Keep `@redocly/cli` and its scripts and CI jobs. Remove the Realm-family packages, `styled-components` and the other template peers nothing else imports (keep `react`/`react-dom` if an island uses them), and the `realm` scripts; add `blume` and the `blume dev`/`build`/`preview` scripts.\n3. Delete `sidebars.yaml` files, `versions.yaml`, `@theme/`, the React pages once rebuilt, `translations.yaml` once mapped, the `tsconfig.json`/`types.d.ts` that only served them, and the redirects file a `$ref` pointed at.\n4. Reunite settings (custom domain, environment variables, teams, hosted analytics) live outside the repo: list what the user must recreate.\n\n## Dropped — report these\n\nLogin, RBAC, and SSO (every protected path); Reunite's editor, reviews, previews, and feedback dashboard; the API explorer's mock server, environments, saved auth, and default CORS proxy (and which APIs now fail CORS); `x-tagGroups` names and the tags they hid; API tag and `section/` pages (redirected); sidebar separators and `expanded: always`; footer column labels; the ` - <title>` suffix now on every `<title>`; dropped frontmatter titles; `seo.image`/`jsonLd` and `llms.txt` sections; extra banners; search facets; resolved `if` branches and per-reader content; `code-snippet` ranges; the Markdoc tags with no equivalent; PlantUML/Excalidraw; light/dark image pairs; unmatched icons; ejected components and Realm CSS; a `UA-` analytics id; scripts that now load only in production; trailing-slash URLs; the accidental pages left out; reset \"last updated\" dates.\n","isInternal":false,"tokens":9842,"sizeBytes":36557},{"name":"starlight.md","path":".agents/skills/blume-migrate/references/starlight.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/starlight.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Starlight → Blume\n\nStarlight is an Astro integration configured via `starlight({…})` in `astro.config.*`, with content in `src/content/docs`. Both are Astro, so this is a natural fit — content can mostly stay in place, with one big caveat: **most Starlight content is `.md`, and Blume's `:::` directives are MDX-only** (see Asides below).\n\n## Detect\n\n- `astro.config.{mjs,mts,ts,js,cjs}` importing and calling **`starlight({…})`** (`import starlight from \"@astrojs/starlight\"`).\n- Content under **`src/content/docs/`**; a `src/content.config.ts` (current) or `src/content/config.ts` (pre-Astro-5).\n- `@astrojs/starlight` dep.\n\nKeep content where it is — set `content.root: \"src/content/docs\"`.\n\n## Config: `starlight({…})` → `blume.config.ts`\n\n**Harvest the surrounding `astro.config.*` too, not just the `starlight()` call:** top-level Astro `redirects` → Blume `redirects`; `site` → `deployment.site`, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md); other integrations → report.\n\n| Starlight option | Blume |\n| --- | --- |\n| `title` | `title` (if per-locale/computed, set manually) |\n| `description` | `description` |\n| `logo` (`{ src }` or `{ light, dark, alt }`) | `logo` — move the referenced file into `public/` |\n| `logo.replacesTitle` | `logo: { text: \"\" }` (renders the mark alone) |\n| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |\n| `social` (array of `{ label, icon, href }`; pre-0.33 legacy: `{ github: url }` object) | derive **`github: { owner, repo }`** from the GitHub entry; every other entry whose platform Blume knows → **`footer.socials`** (`{ discord: href, bluesky: href, … }`; the `x.com`/`twitter` icon → `x`; the platform list is under `footer` in `docs/configuration/index.mdx`); the rest (`mastodon`, `gitlab`, `rss`, …) drop (report) |\n| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it); **an origin other than `https://github.com` → `github.host`** (a GitHub Enterprise repo otherwise links to the public site) |\n| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |\n| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |\n| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |\n| `expressiveCode.themes` | `markdown.code.theme: { light, dark }` — EC assigns by theme _type_, so match each theme by its darkness, not by array position |\n| `expressiveCode.styleOverrides` | drop → restyle via a root `theme.css` file |\n| `head` | per entry: a `meta` tag → **`seo.metatags`** (`{ [attrs.name ?? attrs.property]: attrs.content }` — verification tokens, `theme-color`, and the like), minus the tags Blume writes itself (`description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, `article:*` dates — the build refuses them and names the setting that owns each, e.g. `og:image` → `seo.og`, `twitter:site` → `seo.x.handle`); an analytics `<script>` → one `script()` adapter each in the `analytics` list (from `blume/analytics`): `attrs.src` → `src`, an `async`/`defer` attr → `strategy`, every other `attrs` key → `attributes`, and an inline body (`content`) → `content`; anything else (`link`, `style`, other scripts) → drop and report |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `customCss` | drop → move into a root **`theme.css`** file (auto-picked-up; not a config field) |\n| `components` (overrides) | Blume's layout slots via `defineComponents({ layout: { … } })` in a root `components.ts`: `Header`, `Search`, `Sidebar`, `TableOfContents`, `Pagination` keep their names; `SiteTitle` → `Logo`; **`Footer` → `PageFooter`**, not Blume's `Footer` (that slot is the site footer; Starlight's `Footer` is the page-end last-updated date, pagination, and edit link, which Blume renders itself, so keep only what the override added). Other overrides (`Banner`, `PageTitle`, `Hero`, `EditLink`, `SocialIcons`, …) have no slot — fold their additions into `PageHeader`/`PageFooter`, or `blume eject`; a Starlight name left under `layout` matches no slot and does nothing. Rewrite reads of `Astro.locals.starlightRoute` against the slot's props |\n| `plugins` | **map, don't blanket-drop** — see Plugins below |\n| `pagination: false`, `pagefind`/Pagefind options, `titleDelimiter`, `credits`, `disable404Route`, `routeMiddleware` | drop (report) |\n| `locales` / `defaultLocale` | `i18n` (see below) |\n\n## Navigation: `sidebar`\n\nStarlight's `sidebar` array → prefer letting Blume generate from the filesystem; use `navigation.sidebar` only for shapes files can't express:\n\n- `{ label, items: [{ autogenerate: { directory: \"d\" } }] }` (0.39 and later; before 0.39 the group held it directly, `{ label, autogenerate: { directory: \"d\" } }`) → **structure the folder and rely on filesystem nav** (label → the folder's `meta.ts` `title`; `autogenerate.collapsed` → `collapsed: true` in each subfolder's `meta.ts`). Do **not** map it to a sidebar item with `root:` — in an explicit Blume sidebar that renders a single page link, not the directory's children. Since 0.39 a group's `items` can mix manual entries with an `autogenerate` entry; move those manual pages into the folder (route change → `redirects`) so the folder holds the whole group.\n- string `\"guides/intro\"` → `\"/guides/intro\"`; `{ label?, slug }` → `/<slug>`.\n- `{ label, items: [...] }` → `{ label, items: [...] }` (recursive) — or better, a real folder.\n- `{ label?, link }` → `{ label, href: link }`.\n- `badge` (string or `{ text, variant }`) → `badge` (text only; variant drops). `collapsed` (bool) → `collapsed`. Item `attrs` and `translations` → drop (report).\n\n## Frontmatter\n\n| Starlight | Blume |\n| --- | --- |\n| `title` / `description` / `draft` / `slug` | pass through |\n| `sidebar.{label,order,hidden}` | pass through |\n| `sidebar.badge` | `sidebar.badge` (text only) |\n| `sidebar.attrs` | **remove** (strict schema — build error if left) |\n| `pagefind: false` | `search.exclude: true` |\n| `lastUpdated` (date/string) | `lastModified` |\n| `lastUpdated: false` (boolean) | **remove** (Blume's `lastModified` takes a date; a boolean is a build error) |\n| `prev: false` **and** `next: false` | `pagination: false` (hides both links) |\n| `prev` / `next` otherwise (one side, or a label/link) | **drop** (report — the links can only be hidden together, and their labels come from the linked pages) |\n| `template: splash` | `mode: center` (no sidebar, a wider centered column — the closest layout; report) |\n| `hero` | **no key** — its tagline → a paragraph, its actions → `<Card>`s or links, its image → a Markdown image (report); for a designed landing page, rebuild it as a custom `.astro` page under `content.pages` (`pages/index.astro` on `PageLayout` wins over the content page) |\n| `banner`, `tableOfContents`, `editUrl`, `head` | drop (report) |\n\n## Asides → directives (and the `.md` trap)\n\nStarlight's primary callout syntax is the `:::note`/`:::tip`/`:::caution`/`:::danger` directive **in plain `.md` files**. The directive names map perfectly (Blume aliases `caution`→warning; `error`→danger) and `[Title]` syntax carries over — **but Blume only parses directives in `.mdx`**. In a `.md` file, `:::note` renders as literal text and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. So: **rename every `.md` file that contains asides (or math, or mermaid/package-install fences) to `.mdx`** — for a typical Starlight repo that's most of the content; renaming everything to `.mdx` is usually simpler and safe.\n\n- `<Aside type=\"…\" title=\"…\">` (the component form) → the same directives; bare `<Aside>` → `:::note`.\n- An aside custom icon (`:::tip{icon=\"heart\"}`) → drop the attr (report).\n\n## Components\n\n- **Renames:** `<CardGrid>` → `<CardGroup>`; `<LinkCard>` → `<Card>` (its `description` prop drops — fold into the body); `<TabItem label=\"…\">` → `<Tab title=\"…\">`. `<Tabs>` and `<Card>` stay; **keep** a `<Tabs syncKey=\"…\">` prop as is — Blume's `Tabs` takes `syncKey` with the same scoping (only groups sharing the key switch together). One difference: Blume groups without a key also sync, page-wide, by tab title, where Starlight leaves them independent — add `sync={false}` to a keyless group whose same-titled tabs must stay unlinked.\n- **`<Badge>` needs conversion, not pass-through:** Starlight puts content in a `text` prop and uses variants `note`/`tip`/`caution`/`danger`/`success`/`default` with sizes `small`/`medium`/`large`. Blume's `<Badge>` renders **children** with variants `default`/`accent`/`success`/`warning`/`danger` and sizes `xs`/`sm`/`md`/`lg`. Move `text` into the children; remap variant (`note`→`default`, `tip`→`accent`, `caution`→`warning`, `danger`→`danger`, `success`→`success`) and size (`small`→`sm`, `medium`→`md`, `large`→`lg`).\n- **Convert yourself:** `<Steps>` → Blume `<Steps>`/`<Step>`; `<FileTree>` → Blume `<FileTree>`; `<Code code={…}>` → a fenced code block; `<LinkButton>` → a Markdown link or `<Card>`.\n- Strip `import … from \"@astrojs/starlight/*\"` and `astro:assets` lines.\n\n## Code blocks: Expressive Code meta → Blume\n\nStarlight content is full of Expressive Code fence meta; Blume understands some of it and **promotes unknown bare tokens into the code-block title**, so unconverted meta produces garbage headers. Convert per fence:\n\n- `title=\"file.js\"` → works as-is (or use the space-title shorthand). Line ranges `{2-3}` → work as-is.\n- `ins=`/`del=` line marks → `// [!code ++]` / `// [!code --]` comments; `mark=` → `{ranges}` or `// [!code highlight]`.\n- `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left).\n- `wrap` / `wrap=true` → a bare `wrap` (that block's long lines wrap); a site-wide `expressiveCode.defaultProps.wrap: true` → `markdown.code.wrap: true`.\n- **Drop:** `frame=\"terminal\"`, `collapse=`, `wrap=false`, `preserveIndent`/`hangingIndent`, `\"string\"` and `/regex/` text markers (report if they carried meaning).\n- ` ```diff lang=\"js\" ` → a normal ` ```js ` fence with `[!code ++]`/`[!code --]` markers.\n\n## Plugins — map, don't drop\n\n- `starlight-openapi` → an `openapi({ sources })` entry in Blume's `reference` list, imported from `blume/reference` (delete any generated pages; add the `navigation.tabs` entry).\n- `starlight-blog` → `type: blog` pages (RSS at `/blog/rss.xml`). Blume generates **no** blog index, tag, or author pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag and author pages as dropped.\n- `starlight-versions` → Blume's native versioning, not a `navigation.selectors` dropdown: each archived version's content goes in a top-level folder under `content.root` named for its id, listed in `versions.archived` (newest first), and Blume adds the switcher, the old-version notice, and version-scoped search. Ids must start with a letter (`1.0/` → `v1.0/`, with `redirects` from the old URLs), and a version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as current content. Full reference: `docs/content/versioning.mdx` in the installed package.\n- `starlight-image-zoom` → delete (Blume zooms content images by default).\n- `starlight-links-validator` → delete (`blume validate` covers it).\n- Anything else → report.\n\n## Assets\n\nStarlight co-locates images in `src/assets/` with **relative** references (`../../assets/foo.png`) or `astro:assets` imports; Blume serves `public/` at the site root with absolute URLs. Move `src/assets/*` into `public/`, rewrite relative image paths and `~/`/`@/` aliases to absolute `/…` URLs, and replace `<Image>` imports with Markdown images (or `<Frame>`).\n\n## i18n\n\nStarlight has **two** layouts:\n\n- **`root` locale** (default language at `src/content/docs/`, others in `fr/`, `de/`… subdirs) → matches Blume's `dir` parser as-is: `i18n: { defaultLocale, locales: [{ code, label }] }`, no file moves.\n- **No `root` locale** (every language in a subdir, including the default — `en/…`) → Blume expects the default locale **at the content root**, so move the default locale's files up one level. Starlight served them at `/en/…`, so add a `redirects` entry per page.\n\nDon't restate Blume defaults (`hideDefaultLocalePrefix: true`, `parser: \"dir\"` are already the defaults). Starlight's untranslated-page fallback matches Blume's `fallbackLocale` default.\n\n## Teardown\n\nRemove `@astrojs/starlight` (and plugin deps) from deps, delete the Starlight bits of `astro.config.*` **after harvesting redirects**, delete `src/content.config.ts` / `src/content/config.ts`, repoint scripts to the Blume CLI, add `blume`. A `src/content/docs/404.md` has no direct Blume equivalent — report it (Blume ships its own 404).\n\n## Dropped — report these\n\nSocials on a platform `footer.socials` lacks, badge variants, sidebar/item `attrs` + `translations`, `customCss` beyond `theme.css`, `head` entries other than meta tags and analytics scripts, a page's frontmatter `head`, `routeMiddleware`, splash layouts approximated with `mode: center` and hero blocks rebuilt as content, override components with no Blume slot, starlight-blog's tag and author pages, aside custom icons, EC frames/collapse/text markers, prev/next toggles, unmapped plugins, any `<Icon>` name with no Lucide equivalent.\n","isInternal":false,"tokens":3769,"sizeBytes":13733},{"name":"vitepress.md","path":".agents/skills/blume-migrate/references/vitepress.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/vitepress.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# VitePress → Blume\n\nVitePress is a Vue/Vite static site generator. Its config is a module in `.vitepress/`, navigation is **declared in `themeConfig.sidebar`**, every `.md` page compiles as a **Vue single-file component** (so it can hold Vue components, `{{ }}`, and `<script setup>`), and its Markdown is markdown-it plus VitePress plugins. Watch for five traps:\n\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns (`BLUME_DIRECTIVE_SPACED_NAME`) only for a callout name in `.mdx`. Inside an open `:::` container, the same line closes the container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`). Directives are also MDX-only.\n- **Every page is `.md`, but most need to be `.mdx`.** A page with a callout, component, or math must be renamed, and MDX is stricter than Vue templates.\n- **URLs end in `.html` by default** (`cleanUrls: false`), so every page needs a redirect.\n- **The heading is the body `# H1`, not frontmatter.**\n- **Pages missing from the sidebar are hidden in VitePress.** In Blume they appear unless you hide them.\n\nScope: VitePress 1.x. Notes marked **2.0** cover `2.0.0-alpha.*`.\n\n## Detect\n\n- **A `.vitepress/` directory** holding `config.{js,ts,mjs,mts}` (or `config/index.*`), usually `docs/.vitepress/`. The directory that holds `.vitepress/` is the VitePress **project root**: the argument to `vitepress dev <dir>` in the scripts.\n- **A `vitepress` dependency**, often only in the docs package's `package.json`.\n- **Not VuePress**, which uses `.vuepress/` and `vuepress`/`@vuepress/*` deps (`references/vuepress.md`).\n\n**Where the Blume project goes.** VitePress has a project root, a `srcDir` (the pages, default `.`), and a public directory that is always `<srcDir>/public`. Blume serves `public/` from the directory holding `blume.config.ts`. Two layouts are common:\n\n- The scripts run `vitepress dev docs` from the repo root. Put `blume.config.ts` at the repo root; the default `content.root: \"docs\"` matches (append `srcDir` if one is set). Move `docs/public/` to `public/`.\n- `docs/package.json` runs `vitepress dev`. Put `blume.config.ts` in `docs/`, set `content.root: \".\"`, and scope `include`/`exclude` (`references/monorepo.md` §1). `public/` stays put, but exclude it (VitePress sites keep `.md` there behind `srcExclude`), along with `node_modules`.\n\n## Run the codemod first\n\nBuild the source once (`vitepress build <root>`) and copy `.vitepress/dist` outside the repo (`<old build>` below; skip the build if the user already made one): it's your old URL list, the old heading ids, and the record of what the live site rendered, and Teardown deletes `.vitepress/`. Then, on the VitePress source directory (Blume's `content.root`), dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/vitepress-codemod.mjs <srcDir>          # report only\nnode <skill>/scripts/vitepress-codemod.mjs --write <srcDir>  # apply\nnode <skill>/scripts/vitepress-codemod.mjs --redirects <old build>  # `.html` redirects as JSON\n```\n\nIt's zero-dependency and idempotent. It converts only where the mapping is exact: containers (paired as markdown-it pairs them, see Markdown extensions), code groups, `details`, GitHub alerts, fence `[label]`s and meta, `<<<` snippet imports and `<!--@include:-->` without regions or line ranges, `.html`/`.md`/trailing-slash links backed by a page, `<Badge type text />`, `{#id}`, `[[toc]]`, and the body H1 → `title`. It renames a page to `.mdx` when the result needs MDX (partials keep their names) and makes its comments and `<br>` MDX-safe. **Everything else is a `REVIEW` line**: frontmatter keys Blume rejects, components, Vue syntax, regions and out-of-root snippets, repaired container structure, unconverted includes (including ones inside code fences, which VitePress spliced). Work through that list with the sections below; it's the migration's to-do list.\n\n## Config: `.vitepress/config.*` → `blume.config.ts`\n\nRead the whole module and the helpers it imports (sidebars, nav, per-locale configs). For **2.0**, also read directory-level `config.*` files beside the content.\n\n| VitePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `titleTemplate` | drop (report). Blume's `<title>` is `<page> - <site>`; `seo.title` replaces a page's part |\n| `lang` (not `en-US`) on a single-language site | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`): it sets `<html lang>` and the UI strings, with no switcher or prefix. Use a `code` with a UI pack |\n| `head` | per entry: a known analytics snippet → **that provider's factory** from `blume/analytics` (the gtag loader + `gtag('config', 'G-…')` pair → one `googleAnalytics({ id })`; also GTM, Plausible, …); any other `<script>` → `script()` (`src`, `async`/`defer` → `strategy`, other attrs → `attributes`, inline body → `content`). A `meta` tag → `seo.metatags`, minus the tags Blume writes itself (`description`, `robots`, `og:*`, `twitter:*` card tags; the build names the setting that owns each). `link rel=\"icon\"` → a conventional favicon name in `public/`. Fonts and other tags → drop (report) |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `cleanUrls` | no setting; see \"URLs and redirects\" |\n| `rewrites` | **move each file to the path its rewrite produced**, so the URL stays put (`slug` only where moving is impossible). VitePress relative links are already written against the rewritten paths |\n| `srcDir` / `srcExclude` | `content.root` / `content.exclude`, which adds to the defaults `**/_*` and `**/.*` |\n| `outDir`, `assetsDir`, `cacheDir`, `ignoreDeadLinks`, `metaChunk`, `mpa` | drop |\n| `appearance` | `'dark'`/`'force-dark'` → `theme.mode: \"dark\"`; `false` → `\"light\"`; `true`/`'force-auto'` → omit. Blume always shows the toggle: report a forced mode |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `sitemap.hostname` | `deployment.site`, unless the host is Vercel or Netlify (auto-detected). `sitemap.transformItems` → drop |\n| `markdown.theme` | `markdown.code.theme: { light, dark }`; VitePress's default (`github-light`/`github-dark`) is Blume's, so omit it unless changed |\n| `markdown.lineNumbers: true` | no site-wide switch: add `lineNumbers` per fence, or drop (report) |\n| `markdown.math` | delete `markdown-it-mathjax3`; see Math |\n| `markdown.container` labels, `anchor`, `toc`, `attrs`, `image`, `languages`, `languageAlias`, `headers` | drop (report). A custom `anchor.slugify` changes every anchor |\n| `markdown.config(md)` / `preConfig(md)` with `md.use(…)` | **custom markdown-it plugins: find the syntax each one adds in the content and convert every use** (to Markdown, a directive, or a component); the codemod doesn't know it. Report each plugin |\n| `markdown.codeTransformers` with `@shikijs/vitepress-twoslash` | drop the plugin: ` ```ts twoslash ` works natively |\n| `markdown.externalLinks` | VitePress opens external links in a new tab by default; `markdown.externalLinks: true` keeps that (optional) |\n| `vite`, `vue`, build hooks (`transformHead`, `transformPageData`, `buildEnd`, …) | drop (report what each did). Blume writes canonicals, OG images, `llms.txt`, and the sitemap itself; Astro integrations go in `integrations` |\n| `locales` | `i18n` (see below) |\n\n**`themeConfig`** (default theme):\n\n| `themeConfig` | Blume |\n| --- | --- |\n| `logo` (string, `{ src, alt }`, `{ light, dark, alt }`) | `logo` (string, or `{ image: { light, dark, alt } }`), file in `public/`. A monochrome SVG → `currentColor` and the string form |\n| `siteTitle: false` / a string | `logo: { image: …, text: \"\" }` / `logo.text` |\n| `nav`, `sidebar` | see the Navigation sections |\n| `socialLinks` | GitHub → `github: { owner, repo }` (it renders in the footer); known platforms → `footer.socials` (`twitter` → `x`; list under `footer` in `docs/configuration/index.mdx`); others, including custom `{ svg }` icons → `footer.links`, or drop (report) |\n| `footer` (`message`, `copyright`) | `copyright` → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`); a per-locale `copyright` → a locale map. `message` → drop (report); `footer.links` takes `{ label, href }` |\n| `editLink.pattern` (`…/edit/<branch>/<path>/:path`) | `github: { owner, repo, branch }`. `<path>` leads to the **srcDir**, but Blume builds edit links from the Blume project dir, so `github.dir` is `<path>` minus `content.root` (omit when empty). A non-`github.com` origin → `github.host` |\n| `lastUpdated.formatOptions` | its date fields → `dateFormat`; `text` → drop |\n| `search` | `local` → omit. **Algolia → omit `search`** (Blume's built-in Orama), remove the DocSearch deps, and report the `appId`/`indexName`. That index is almost always DocSearch's, filled by Algolia's crawler, and Blume's sync, given an admin key, replaces the whole index on every build: never point `algolia()` at it. `search: algolia({ appId, apiKey, indexName })` from `blume/search` is for a new index Blume owns, a separate opt-in |\n| `outline` (`2`, `[2, 4]`, `'deep'`, `false`) | `toc` (`{ minHeadingLevel, maxHeadingLevel }`; `'deep'` → 2–6; `false` → `toc: false`), only when declared |\n| `aside`, `docFooter`, the `*Label` strings, `externalLinkIcon`, `carbonAds`, **2.0** `gradedContainers`, theme-package keys | drop. UI strings come from Blume's i18n packs (`i18n.ui`) |\n\n## Navigation: `sidebar`\n\nVitePress shows only what `sidebar` lists, in that order. Make the file tree match it, carrying labels, order, and collapse into `meta.ts`; reach for `navigation.sidebar` only for shapes files can't express.\n\n- **Multiple sidebars** (`{ '/guide/': [...] }`) → one **`navigation.tabs`** entry per key (`{ label, path: \"/guide\" }`, label from the `nav`). Give each tab folder a `meta.ts` `title` matching its tab label. VitePress sidebars rarely list root pages, so once those are hidden, a root route has no loose pages to show and Blume falls back to the full tree, where an untitled tab folder shows its humanized folder name.\n- **A group whose pages sit flat in one folder** (the nesting exists only in config) → **a `(group)` folder**: `docs/(fundamentals)/controllers.md` still serves `/docs/controllers`, so no URL changes and no redirects. Give it a `meta.ts` (`title`, `pages` in the listed order). A listed page from another folder can join with frontmatter `slug` keeping its old route. **Moving a page one folder deeper breaks its relative links and assets** (`./assets/x.png`, `../other.md`): re-base them, or make them root-absolute.\n- **`collapsed`**: unset → not collapsible (Blume's `flat`); `false` → `display: \"group\"` + `collapsed: false`; `true` → `display: \"group\"` + `collapsed: true`, in that folder's `meta.ts`.\n- **A group with its own `link`** → that page becomes the folder's `index` (its URL changes: redirect the old one).\n- **Item `text` that differs from the page's title** → `sidebar.label`. **`base`** → prefix the child links before mapping. External items → `navigation.featured`. `docFooterText`, `rel`, `target` → drop.\n- **Pages no sidebar lists** → `sidebar.hidden: true`. A page listed in two sections, or a cross-section link, can't repeat in filesystem nav: keep one place and report.\n\n## Navigation: `nav`\n\n- A link to a section a sidebar key backs → a `navigation.tabs` entry. **A dropdown whose items mirror that section's sidebar groups** (a mega-menu) → a plain tab: the sidebar already shows them.\n- A dropdown of pages in one section that isn't the sidebar → a tab with `items` (flat; nested groups flatten).\n- External or utility links → `navigation.actions`; a primary call to action → `navigation.cta`. A version dropdown → drop its version links (report), but move its utility links (Releases, Team, Contributing) to `footer.links` or `navigation.actions`, so pages linked only from there stay reachable.\n- `activeMatch`, `target`, `rel`, `noIcon` → drop.\n\n## Titles\n\nVitePress takes a page's heading from its first `# H1`; frontmatter `title` only sets `<title>`. Blume renders frontmatter `title` as the H1, so **a body H1 beside a `title` shows twice**; a page with no `title` takes it from the `# H1` that opens its body and shows it once. The codemod moves a plain H1 into `title` and deletes it. It reports two cases: an H1 with inline markup (move it by hand), and a frontmatter `title` that differs from the H1 (make the H1 `title`, keep the old value as `seo.title`). A home page with no H1 takes `hero.name` / `hero.text`.\n\n## Markdown extensions\n\nVuePress shares the container and fence conventions; its snippet and include syntax differ (see `references/vuepress.md`). **What Blume renders below is MDX-only unless marked `.md` OK.** Of these targets, plain `.md` handles fence titles and meta, `// [!code …]` notations, `{#id}`/`[#id]` anchors, and `<include>`; callouts, components, and math need `.mdx`.\n\n**Container pairing.** markdown-it-container scans each container's lines for its closer, so **a bare `:::` closes the outermost open container whose fence is no longer than it, and everything nested in it**. An unclosed container runs to the end of its parent (the page, for a top-level one), and a `:::` with nothing to close shows as text. Real sites have unclosed code groups that swallow the next callout, closers that close two containers, and stray closers. The codemod pairs this way, ends a code group at its last code block when other content follows inside it (a group only holds code), drops stray closers, gives a nested callout's outer fence the extra colon Blume needs, and reports every repair. Check the old build's HTML for literal `:::` to see what the live site already showed broken.\n\n| VitePress | Blume |\n| --- | --- |\n| `::: tip` / `info` / `warning` / `danger` (spaced) | `:::tip` etc., with **no space**; `::: tip` is never parsed (codemod) |\n| `::: danger STOP` | `:::danger[STOP]`. `:::danger STOP` renders without `STOP`, and `.mdx` warns `BLUME_DIRECTIVE_OPENING_TEXT` (codemod) |\n| `::: note` / `important` / `caution` | **2.0** containers → `:::note`, `:::note`, `:::danger` (`:::warning` with `gradedContainers`). On **1.x** they aren't containers and showed as text; converting changes that: report (codemod) |\n| `::: details Title` (`{open}`) | `<Expandable title=\"Title\">` (`defaultOpen`); untitled → `title=\"Details\"` (codemod) |\n| `> [!NOTE]` `[!TIP]` `[!IMPORTANT]` `[!WARNING]` `[!CAUTION]` (+ `[!INFO]`, `[!DANGER]`) | `:::note` / `tip` / `note` / `warning` / `danger` / `info` / `danger`; text after the marker → `[Title]` (codemod). Unconverted, the five GitHub markers still render as those callouts in `.mdx` (a lazy continuation included); in `.md` they're plain quotes with a literal `[!NOTE]` that warn `BLUME_MD_GITHUB_ALERT` (rename the page to `.mdx`), and `[!INFO]`/`[!DANGER]` are plain quotes in both, with no warning. The codemod leaves lazy-continuation and indented alerts for you |\n| `::: raw`, `::: v-pre` | remove the wrapper lines (codemod) |\n| `::: code-group` + `[label]` fences | `<CodeGroup>`; each block's title is its label (bare token, or `title=\"Two words\"`), else its language as VitePress showed. Blume syncs same-titled tabs page-wide (codemod) |\n| Raw `<div class=\"tip custom-block\">…</div>` (hand-written container markup) | the matching directive |\n| ` ```js{4} ` / ` ```ts:line-numbers ` / ` ```js [file.js] ` | ` ```js {4} ` (Blume reads it glued too) / ` ```ts lineNumbers ` (`=N` start → drop) / ` ```js file.js ` (a bracketed title also shows without its brackets; `.md` OK; codemod) |\n| `// [!code ++]` `--` `focus` `highlight`/`hl` `error` `warning` | same (`.md` OK) |\n| `js-vue` | plain `js`; resolve its `{{ }}` by hand |\n| `<<< @/path/file.ts{2,4} [Label]` (`{lang}`, `:line-numbers`) | `<include meta=\"Label {2,4}\">/path/file.ts</include>` (`lang=\"…\"`, `lineNumbers`); `@` is the srcDir, `/` Blume's content root. In a code group VitePress titles an unlabeled import with its file name. A `.md` target gets `lang=\"md\"`: VitePress showed it as code, Blume would splice it (`.md` OK; codemod) |\n| `<<< file#region` | no region selection in `<include>`: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (selects `#region` markers; set `\"dedent\": true`, since VitePress always dedents regions; runs before every build, so the excerpt keeps tracking the source). Where VitePress couldn't find the region it showed the whole file, so check the old build |\n| a snippet outside the content root (`@/../packages/…`) | `BLUME_INCLUDE_OUTSIDE_ROOT` fails the build: copy the file under `_snippets/` and report |\n| `<!--@include: ./part.md-->` | `<include>./part.md</include>` (`.md` OK; codemod). A line range or region → split the partial. VitePress publishes a partial as a page unless `srcExclude` hides it: prefix partials with `_` (excluded by default) and update the paths. A partial is parsed in the including page's format, so one spliced into `.mdx` must be MDX-safe |\n| `[[toc]]` | delete; Blume has the outline rail (codemod) |\n| `:tada:` emoji shortcodes | paste the emoji. A trailing emoji leaves no dash in the id (`## Emoji 🎉` → `#emoji`); the anchor script pins any old id that differs |\n| `## Heading {#id}` | `[#id]` in both formats; a bare `{#id}` fails `.mdx` (codemod) |\n| markdown-it-attrs (`[link](/x){target=\"_self\"}`, `{.class}`) | delete (codemod, for links) |\n| `$…$` / `$$…$$` (`markdown.math`) | `$$…$$` in `.mdx`, block or inline; convert each inline `$…$`, checking it's math |\n| ` ```mermaid ` (`vitepress-plugin-mermaid`) | as written in `.mdx`; delete the plugin |\n| `:::tabs` + `== Label` (`vitepress-plugin-tabs`) | `<Tabs>` of `<Tab title=\"Label\">`, or `<CodeGroup>` when every tab is one fence |\n| Containers a site registers itself (markdown-it-container in `markdown.config`, **2.0** `customContainers`) | read the registration, then map each to a callout, `<Tabs>`, or a component (report). The codemod pairs them as containers and leaves them as written, lengthening an enclosing callout's fence: Blume ends a `:::` callout at a `::: name` line inside it and drops the line |\n\n## Vue in Markdown\n\nVitePress pages are Vue templates; Blume pages aren't. **In `.mdx` these fail the build**: an HTML comment, `{{ … }}`, a `:prop=` binding, `<script setup>`, an unclosed `<br>`/`<img>`, an `<https://…>` autolink, and `{name}` in prose. **In `.md` they pass through as text or raw HTML** with no error. The codemod reports each.\n\n- **`{{ expr }}`, `$frontmatter`, `$params`** → substitute the value; shared values → Blume `variables`; literal braces → inline code. Vue directives on HTML (`v-if`, `v-for`, `v-pre`) → static content.\n- **`<!-- comments -->`** → `{/* … */}` in `.mdx` (the codemod converts one-line ones).\n- **`<script setup>`, `<script>`, `<style>`** → delete; move styles to a root `theme.css`.\n- **`<Badge type text />`** → `<Badge variant>text</Badge>` (`tip`, the default → `accent`; `info` → `default`; `warning`, `danger` as named; **2.0** `note` → `default`, `important` → `accent`, `caution` → `danger`). Blume leaves a badge's text out of the heading's id (`## Title <Badge>beta</Badge>` → `#title`), as VitePress did for a self-closing badge, but VitePress put a badge's children in it (`<Badge type=\"info\">beta</Badge>` → `#title-beta`), so the codemod pins each converted badge heading to its VitePress id (`[#title-beta]`; `[#title]` for a self-closing badge, redundant but harmless). Unconverted in `.mdx`, a `<Badge type text />` renders empty and warns `BLUME_UNKNOWN_PROP`.\n- **Components** (global ones from the theme's `enhanceApp` or a theme package, and imported `.vue` files): decide per component: a Blume component, Markdown, a static `.astro` component in `components.ts`, or the `.vue` file as an island (`islands/`, with `@astrojs/vue vue` installed; `:users=\"['a']\"` → `users={[\"a\"]}`). Components importing from `vitepress` (`useData`, theme parts) can't run outside it. **First check the old build: a component the theme never registered rendered nothing** (`<!---->` in the HTML), so converting it shows content readers never saw. Report it. Data-driven theme components with no source of data (API lists, contributor avatars) → drop, delete headings left empty, and report the lead-in sentences that now point at nothing.\n- **Raw `<img>`/`<figure>` with a relative `src`** → a Markdown image, which is optimized. Left as is, it works: Blume publishes a file a relative `src` names beside the page, unoptimized, and `blume validate` checks it.\n- **`<ClientOnly>`** → drop the wrapper (an island that needs `window` uses `client: \"only\"`). **`VPTeamMembers`** → a `<CardGroup>` of `<Card>`s, or drop (report).\n- **Data loaders** (`*.data.{js,ts}`) → inline the data, compute it in a custom `.astro` page, or use a content source (report). **Dynamic routes** (`[param].md` + `.paths.*`) → write one page per generated path, then delete the template.\n\n## Home page (`layout: home`)\n\n- **The site home** → `index.mdx` with `mode: custom` (no title or description rendered, so write the heading): the hero as heading, paragraph, and links; each feature as a `<Card title href icon>` in a `<CardGroup cols={3}>` (an image path or inline `<svg>` icon passes to `icon`; an emoji has no slot). For a designed landing page, `pages/index.astro` on `PageLayout`.\n- **Any other page with `layout: home`** (a team or about page using only the hero) → default doc mode: `hero.name` → `title`, `tagline` → a paragraph.\n- Hero CSS variables and theme-package keys (`testimonial`, `features` styling) → drop (report).\n\n## Frontmatter\n\n| VitePress | Blume |\n| --- | --- |\n| `title` | see Titles |\n| `description` | pass through |\n| `head` | `meta name=description` → `description` (when the page has none; VitePress didn't emit a page's `head` description beside a frontmatter one); `og:image` → `seo.image`; `link rel=canonical` → `seo.canonical`; `robots` `noindex` → `noindex: true`; `keywords` → drop; anything else → drop (report) |\n| `layout: doc` | remove |\n| `layout: page` | `mode: frame`. **If the page's only content was a component you're dropping, use the default mode** so the title renders |\n| `layout: home` | see Home page |\n| `layout: false` / a custom layout | `mode: custom`, or a custom `.astro` page (report) |\n| `sidebar: false` | `mode: center` |\n| `aside: false` / `outline: false` | `mode: wide`, or drop; `outline` levels, `aside: left` → drop (report) |\n| `prev: false` **and** `next: false` | `pagination: false`; other `prev`/`next` → drop (report) |\n| `lastUpdated: <date>` | `lastModified`; `false` → remove |\n| `titleTemplate`, `editLink`, `footer`, `navbar`, `pageClass`, `markdownStyles`, theme-package keys | drop (report), or declare with `frontmatter.extend` if a component you wrote reads them |\n\n## Icons\n\nVitePress has no page-icon system. Icons you add (tabs, `meta.ts`, cards) are kebab-case Lucide names; an inline Lucide SVG (`class=\"lucide lucide-server\"`) maps to that name (`server`).\n\n## Assets\n\n- **`<srcDir>/public/`** → `public/` beside `blume.config.ts`; root-absolute references keep working.\n- **Relative images** work as written and are optimized, **except draw.io/diagrams.net SVG exports**: the `content=\"<mxfile…>\"` attribute on their `<svg>` hides the size Astro reads, so Blume serves the file as it is and warns `BLUME_SVG_UNOPTIMIZED` at the line that embeds it. Strip that attribute to have it optimized.\n- **`.html` files under `public/`** (`/pure.html`) → keep the links.\n\n## i18n\n\nA `root` locale with others in `fr/`, `zh/` folders matches Blume's `dir` layout: `i18n: { defaultLocale, locales: [{ code, label }] }`, `code` = the folder (not `lang`), `defaultLocale` = the root's language (`en-US` → `en`). Per-locale nav labels → label maps on `navigation.tabs`/`actions`; per-locale sidebars → each locale's `meta.ts`; per-locale `title`/`description` → drop (report). Pin translated headings to their own old ids: translated pages keep their own old anchors, and the anchor script (see URLs and redirects) pairs each page with its own old page, never with the default locale's. With every locale in a folder (`en/` too), move the default up to the root and redirect `/en/…`, or set `hideDefaultLocalePrefix: false`.\n\n## URLs and redirects\n\nThe old URL list is `<old build>/**/*.html` (or the live `sitemap.xml`). Blume serves `/guide/foo` for `guide/foo.md` and `/guide` for `guide/index.md`.\n\n- **`cleanUrls: false` (the default): one exact redirect per non-index page,** `{ from: \"/guide/foo.html\", to: \"/guide/foo\" }`. `vitepress-codemod.mjs --redirects <old build>` prints them all; paste the array into `redirects`. A pattern can't express a `.html` suffix (`/:path*.html` fails validation).\n- **Don't redirect an index URL** (`/guide/`, `/guide/index.html`, `/index.html`): static hosts serve Blume's `guide/index.html` there already, and `blume dev`/`preview` redirect `/guide/` to `/guide`. `from: \"/guide/\"` takes over the page (`BLUME_REDIRECT_MATCHES_PAGE`); `from: \"/guide/index.html\"` builds (no redirect page is written over the page) but is unneeded.\n- **`cleanUrls: true`:** old URLs already match.\n- **A moved route** (undone `rewrites`, a renamed `_`-prefixed page, a numeric prefix Blume strips, a `foo.md` beside `foo/index.md`, which is one route in Blume: `BLUME_DUPLICATE_ROUTE`) → redirect both the `.html` and the clean spelling.\n- **Internal links:** the codemod rewrites `.html`, absolute `.md`, and trailing-slash links that a page backs; it reports the rest. `blume validate` reports each `.html` link left (`BLUME_BROKEN_ASSET`, naming the page's route when one matches). Fix links in config, frontmatter (`hero.actions`), and raw `<a href>` too.\n- **Anchors.** VitePress slugs headings its own way (punctuation runs → one `-`, leading digit → `_`); Blume uses `github-slugger` (punctuation dropped). On a large site hundreds of ids change (`#what-s-new` → `#whats-new`, `#tsed-core` → `#tsedcore`). After the first build, run the anchor script against the old build: `node <skill>/scripts/pin-heading-ids.mjs --old <old build>` (`--write` to apply), then rebuild and rerun it until it reports 0. By default it pins every id that changed; `--only-linked` pins only the ids content links to, for a site where hundreds change, and is a choice to report, since outside deep links to the rest then land at the top of the page. It reads the ids VitePress published, so a custom `markdown.anchor.slugify` is covered too, and it finds each old page at its own route, so add `--map` only for pages that moved. A `NOTE` names a heading it couldn't pin: pin it by hand (`## What's new [#what-s-new]`) if the heading still exists. A pin **replaces** the generated id, so run `blume validate --strict` after: any `BLUME_BROKEN_ANCHOR` left was already dead on the old site (retarget it or drop the fragment, and report it).\n- **Verify** in `dist/`: every old URL is either a built page (`dist/<route>/index.html`) or a `from` in `dist/_redirects` / `dist/vercel.json` whose target is a built page. Static builds also write a meta-refresh page at `dist/<route>.html/index.html`. `blume preview` serves both, applies the redirects as the host files do, and redirects `/<route>/` to `/<route>`, so you can click through old URLs there. On Netlify, set `deployment: netlify({ output: \"static\" })`: only its `_redirects` forces the rules, so a build for no named host gets the meta-refresh page with a 200. How GitHub Pages serves `/<route>.html` is unverified, so check a few old URLs after deploying there.\n\n## Teardown\n\n- Remove `vitepress`, its plugins (`vitepress-plugin-*`, `@shikijs/vitepress-twoslash`, `markdown-it-*`), the theme package, and `vue` (keep `vue` + `@astrojs/vue` for Vue islands). Delete `.vitepress/` (once its `dist/` is copied out), data loaders, `.paths.*` files, and PostCSS/Tailwind configs that only served the theme. Theme CSS worth keeping → a root `theme.css` against Blume's tokens (`--vp-c-brand-1` → `theme.accent`).\n- **Check the `tsconfig.json` in the Blume project dir**: Blume's build reads it, so drop `.vitepress` includes and make sure `extends` resolves from that dir (install the root workspace first), or delete it if it only served VitePress. An unresolvable `extends` fails the build (`BLUME_TSCONFIG_EXTENDS`, at the line that names it).\n- Repoint `docs:dev`/`docs:build`/`docs:preview` to `blume dev`/`build`/`preview`. Grep the repo for `.vitepress/dist` and `vitepress build`: CI and deploy configs (Pages workflows, publish scripts, `netlify.toml`) must now publish `dist/` and install the docs package's deps. `.gitignore`: `.vitepress/cache`, `.vitepress/dist` → `.blume`, `dist`.\n- A `404.md` → delete (Blume ships a 404; `pages/404.astro` replaces it).\n\n## Dropped — report these\n\n`titleTemplate`, the footer `message` (and any link inside `copyright`), forced `appearance`, per-page `outline`/`aside`, one-sided `prev`/`next`, page-level toggles (`editLink`, `footer`, `navbar`, `pageClass`), `head` entries beyond meta tags and analytics, build hooks, Vite and custom markdown-it plugins, unsupported social links, line-number offsets and the site-wide switch, snippet regions and include ranges (now split files), copied out-of-root snippets, data-driven and unregistered theme components (and any content they hid), `VPTeamMembers`, data loaders and dynamic routes, hero styling, the DocSearch index (with its credentials), old-site anchors that were already broken (and any left unpinned under `--only-linked`), version-dropdown links, and `::: note`/`important`/`caution` text that 1.x showed literally.\n","isInternal":false,"tokens":8249,"sizeBytes":29912},{"name":"vuepress.md","path":".agents/skills/blume-migrate/references/vuepress.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/references/vuepress.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# VuePress → Blume\n\nVuePress is the Vue static site generator that VitePress succeeded. Its config is a module in `.vuepress/`, navigation is **declared in the theme's `sidebar`**, every `.md` page compiles as a **Vue single-file component**, and its Markdown is markdown-it with `::: name` containers. Most of that layer matches VitePress, so **this file covers only what differs, and sends you to a named section of `references/vitepress.md` for the rest. Keep that file open too.** Watch for six traps:\n\n- **`README.md` is a folder's index page in VuePress, but an ordinary page in Blume.** `guide/README.md` publishes at `/guide/README`, and the folder URL `/guide` has no page. Rename every `README.md` to `index.md` (or `.mdx`), then fix the links to it (see \"URLs and redirects\").\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns only for a callout name in `.mdx`; inside an open `:::` container the line closes it instead. See vitepress.md § \"Markdown extensions\".\n- **URLs end in `.html`** (`/guide/getting-started.html`). Every non-index page needs a redirect.\n- **v1 sidebar groups are collapsible unless `collapsable: false`** (note the spelling). That's the opposite of VitePress and of VuePress 2.\n- **A page's title is its frontmatter `title`, falling back to the body `# H1`, and a page often has both, with different text.** See \"Titles\".\n- **Theme components fail silently.** `<code-group>`, `<code-block>`, and `<router-link>` have hyphenated names, so they pass through as unknown HTML elements, in `.mdx` too. A code group renders every block stacked, and a router link renders with no `href`. Neither `build` nor `validate` reports them.\n\nScope: **VuePress 1** (`vuepress@1.x`, Vue 2, webpack) with the default theme or a theme that `extend`s it, in full. **VuePress 2** (`vuepress@2.0.0-rc.*`, Vue 3, `@vuepress/theme-default` 2.x) is covered in notes marked **v2**. `vuepress-theme-hope` (v2) has its common syntax under \"Themes\". For any other theme (`vuepress-theme-reco`, `vuepress-theme-plume`, an ejected `.vuepress/theme/`), inventory its components and frontmatter by hand. The v1 mappings that VuePress's own docs exercise were run end to end on them; the rest of v1 (`permalink`, `base`, `vuepress-plugin-tabs`, the blog plugin, …), the **v2** notes, and the hope section come from source and docs and haven't been run.\n\n## Detect\n\n- **A `.vuepress/` directory** holding `config.{js,ts}`, or v1's `config.yml`/`config.toml`, or v2's `config.mjs`. It's usually `docs/.vuepress/`. **v2** also reads `vuepress.config.{ts,js,mjs}` from the directory the CLI runs in. The directory that holds `.vuepress/` is the **source dir**: the argument to `vuepress dev <dir>` in the `package.json` scripts.\n- **Dependencies:** `vuepress` (both majors), the v2 betas' `vuepress-vite`/`vuepress-webpack`, or `@vuepress/*`, `vuepress-plugin-*`, and `vuepress-theme-*` packages. They're often only in the docs package's own `package.json`.\n- **Which major:** a `vuepress` version of `^1` means v1. A `2.0.0-*` version, `defineUserConfig`, `bundler: viteBundler()`, or `theme: defaultTheme({…})` means v2. A v1 config is CommonJS (`module.exports`) or `defineConfig` from `vuepress/config` (1.9+), with the theme's options under `themeConfig`. A function config (`ctx => ({…})`) may branch on `ctx.isProd`, so read both sides.\n- **Not VitePress.** VitePress uses `.vitepress/` and a `vitepress` dependency (`references/vitepress.md`).\n\n**Where the Blume project goes:** `blume.config.ts` goes in the directory the docs scripts run in (the `package.json` holding `vuepress build docs`, which may be a workspace package like `packages/docs`, not the repo root), with `content.root` set to the source dir (the default `docs` usually matches). When the scripts run `vuepress build` inside the source dir itself, use vitepress.md § \"Detect\"'s second layout. The public directory is `<source dir>/.vuepress/public` (in **v2**, the `public` option). Move it to `public/` beside `blume.config.ts`. Blume's default `exclude` (`**/.*`) already skips `.vuepress/`.\n\n## The VitePress codemod\n\nvitepress.md's codemod (`<skill>/scripts/vitepress-codemod.mjs`) also reads VuePress 1 pages. Over VuePress's own docs, with `README.md` already renamed to `index.md`, it converted containers, `details`, `v-pre`, comments, `<Badge>`s (pinning their heading anchors to the old ids, which Blume now gives them anyway), `[[toc]]`, glued fence ranges, body H1s, and links that a page backs, and listed much of the rest as `REVIEW` lines. Run it on the source dir **after** the `README.md` renames (before them, links to folder indexes come back as unbacked; it leaves `.vuepress/` alone), then correct what it treats as VitePress:\n\n- **Not reported at all:** lowercase and hyphenated tags (`<code-group>`, `<code-block>`, `<router-link>`, `<demo-1>`; `<Foo-Bar/>` is reported as `<Foo>`), `:emoji:` shortcodes, `@flowstart` blocks, `sidebar: auto` (which then fails the build), **v2** `@[code]` imports, and relative `README.md` links, which stay as written. Grep for each, and rewrite the `README.md` links to the folder (see \"URLs and redirects\").\n- **`<code-group>` and `<code-block>`** aren't converted, and a page with nothing else to convert stays `.md` (VuePress's English `guide/getting-started.md` did). Convert them, and rename the page to `.mdx`. **v2** `::: code-tabs` / `::: tabs` come back as unknown containers to convert by hand.\n- **The v2 betas' `:::: code-group` + `::: code-group-item Title`** is read as VitePress's `::: code-group`: it comes out as an empty `<CodeGroup>` followed by the items, with a removed \"stray\" closer. Convert those blocks by hand before running it.\n- **v1 badge types** `error`, `warn`, and `yellow` come out as `accent`: map them as \"Vue in Markdown\" says.\n- **A frontmatter `title` that differs from the H1** is reported for `seo.title`. In VuePress it was the sidebar text, so it goes to `sidebar.label` (see \"Titles\").\n- **`<<< @/…`** resolves `@` against the source dir, which is wrong when `vuepress` ran elsewhere. Check each import it converts or reports against the `<<<` row below.\n- **`--redirects`** reads only VitePress builds: on a VuePress build it prints `[]`, which doesn't mean no redirects are needed. Use the command in \"URLs and redirects\".\n\n## Config: `.vuepress/config.*` → `blume.config.ts`\n\nRead the whole config, including the modules it imports. VuePress sites often keep `nav` and `sidebar` in per-locale files under `.vuepress/config/`.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `head` | same as VitePress `head` (vitepress.md § \"Config\"); the `[tag, attrs, content]` format is identical. `link rel=\"apple-touch-icon\"` → copy the file to `public/apple-touch-icon.png`, which Blume detects. `manifest` and `mask-icon` links → drop with the PWA |\n| **v2** `lang` | same as VitePress `lang` |\n| `locales` | `i18n` (see \"i18n\") |\n| `dest`, `temp`, `cache`, `host`, `port`, `open`, `shouldPrefetch`, `shouldPreload`, v1 `devTemplate` / `ssrTemplate`, **v2** `templateDev` / `templateBuild`, `evergreen`, `extraWatchFiles`, `debug` | drop |\n| `patterns` / **v2** `pagePatterns` | `content.include` / `exclude`. v1's default pattern also builds `.vue` files as pages: rebuild each as Markdown or a custom `.astro` page (report) |\n| `permalink` (v1, site-wide) / **v2** `permalinkPattern` | no setting. Give each affected page a `slug` that reproduces its old URL (see \"URLs and redirects\") |\n| **v2** `public` | the directory to move to `public/` |\n| `theme` | see \"Themes\" |\n| `markdown.lineNumbers` (v1) | no site-wide switch. Add `lineNumbers` to the fences that need it, or drop it (report) |\n| `markdown.slugify` | a custom slugify changes every anchor. See vitepress.md § \"URLs and redirects\" |\n| `markdown.anchor`, `markdown.toc`, `markdown.pageSuffix`, `markdown.extractHeaders` / **v2** `markdown.headers` | drop. If `extractHeaders` added `h4`, set `toc: { maxHeadingLevel: 4 }` |\n| `markdown.externalLinks` / **v2** `markdown.links.externalAttrs` | VuePress opens external links in a new tab by default. To keep that, set `markdown.externalLinks: true` (optional) |\n| `markdown.extendMarkdown`, `chainMarkdown`, `markdown.plugins` / **v2** `extendsMarkdown` (also inside local plugins) | custom markdown-it plugins. Find the syntax each adds and convert every use, as in vitepress.md's `markdown.config` row. Report each plugin |\n| **v2** `markdown.importCode.handleImportPath` | aliases for `@[code]` paths. Apply them as you convert those imports |\n| `configureWebpack`, `chainWebpack`, `postcss`, `stylus`, `scss`, `sass`, `less`, `alias`, `define`, **v2** `bundler` | drop (report) |\n\n**Theme options** (v1 `themeConfig`; **v2** the `defaultTheme({…})` options):\n\n| VuePress | Blume |\n| --- | --- |\n| `logo` | `logo`. Move the file into `public/`. **v2** `logoDark` → `logo: { image: { light, dark } }`, and `logoAlt` → `alt` |\n| `nav` / **v2** `navbar` | see \"Navigation: `nav`\". `navbar: false` → drop (report) |\n| `sidebar` | see \"Navigation: `sidebar`\" |\n| `sidebarDepth`, `displayAllHeaders`, `activeHeaderLinks` | drop. VuePress listed a page's headings in the sidebar; Blume lists them in the outline rail (`h2`–`h3`, set by `toc`) |\n| `repo` (`owner/name` means GitHub) | `github` adds the footer mark **and** an Edit on GitHub action, so set it only where the site had edit links: v1 with `editLinks: true`, **v2** unless `editLink: false`. Otherwise keep just the mark: `navigation.repo: \"https://github.com/<owner>/<name>\"`. A GitLab or Bitbucket URL → a `navigation.actions` link; edit links drop (report) |\n| `docsRepo` / `docsBranch` / `docsDir` (edit links) | `github: { owner, repo, branch, dir }`, from `docsRepo` when it's set (then point `navigation.repo` at `repo`). `docsBranch` defaults to `master` in v1 and `main` in **v2**, so set `branch` to the real branch. `docsDir` is the repo path to the source dir, so `github.dir` is `docsDir` minus `content.root` (`packages/docs/docs` → `packages/docs`). **v2** `editLinkPattern` → read the path from it the same way |\n| `lastUpdated` | v1 turns the date on when `lastUpdated` (a label or `true`) is set in `themeConfig` **or in any `themeConfig.locales[path]`**: either → `lastModified: \"git\"`, and the label drops. **v2** shows it unless `lastUpdated: false`; map it only when the config sets it, and report the default otherwise |\n| `nextLinks: false` **and** `prevLinks: false` (v1) | `pagination: false` on every page: there's no site-wide switch. Only one of them → drop (report) |\n| `search: false` | `search: false` |\n| `algolia` (`apiKey`, `indexName`, `appId`), in `themeConfig` or, in v1, any `themeConfig.locales[path]` | omit `search` (Orama) and report the credentials. A v1 `algolia` block is almost always **DocSearch**: Algolia's crawler fills the index (the app is `BH4D9OD16A` when `appId` is missing, and `algoliaOptions.facetFilters` often marks a shared index). Its records aren't Blume's, and Blume's sync, given an admin key, replaces the whole index on every build. Never point `algolia()` at it; a new index of your own is a separate opt-in |\n| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` → report: Blume always shows the toggle |\n| **v2** `hostname` | `deployment.site`, unless the target host is Vercel or Netlify |\n| `locales` | see \"i18n\" |\n| `smoothScroll`, `searchMaxSuggestions`, `searchPlaceholder`, `repoLabel`, `editLinkText`, **v2** `contributors`, `externalLinkIcon`, `home`, `notFound`, `backToHome`, and the other UI labels | drop. A translated UI string → `i18n.ui` |\n\n**Plugins** (`plugins`, plus the ones the theme turns on):\n\n| VuePress | Blume |\n| --- | --- |\n| `@vuepress/plugin-search` (v1's built-in search), **v2** `search`, `slimsearch`, `flexsearch`, `orama` | omit: Orama is the default |\n| **v2** `@vuepress/plugin-meilisearch` | omit, and report: Blume has no Meilisearch adapter |\n| **v2** `@vuepress/plugin-docsearch` | as the `algolia` row: omit `search` and report |\n| `@vuepress/plugin-google-analytics` (v1 `{ ga }`, **v2** `{ id }`) | `analytics: [googleAnalytics({ id })]` from `blume/analytics`. A `UA-` id stopped collecting when Google retired Universal Analytics, so ask for the GA4 `G-` id, or drop it (report) |\n| Other analytics plugins | the matching `blume/analytics` adapter, or `script()` |\n| `vuepress-plugin-sitemap`, **v2** `@vuepress/plugin-sitemap` / `plugin-seo` (`hostname`) | `deployment.site` (with the same host rule as above). Blume writes the sitemap, canonicals, and Open Graph tags itself |\n| `@vuepress/plugin-medium-zoom` | omit: Blume content images zoom on click by default |\n| `@vuepress/plugin-back-to-top` | omit: Blume's page actions include \"Scroll to top\" |\n| `@vuepress/plugin-pwa` | drop (report). Blume has no service worker. The `manifest` `head` link drops with it |\n| `@vuepress/plugin-last-updated` | `lastModified: \"git\"` |\n| `nprogress`, `active-header-links`, `smooth-scroll`, **v2** `copy-code`, `git` | drop |\n| `@vuepress/plugin-register-components` | see \"Vue in Markdown\" |\n| `container` (`vuepress-plugin-container`) / **v2** `@vuepress/plugin-markdown-container` | containers the site registers. Read each entry's `type`, `before`, `after`, and `defaultTitle`, then map it as in vitepress.md's last \"Markdown extensions\" row (report) |\n| **v2** `@vuepress/plugin-redirect` | its `config` map → `redirects`. `redirectFrom` frontmatter → one `redirects` entry per old path, pointing at that page. A page with `redirectTo` → delete it and add a redirect |\n| `@vuepress/plugin-blog`, `vuepress-plugin-blog` | `type: blog` pages plus a hand-written index (see `docs/advanced/blog.mdx`). Tag, category, and pagination pages → drop (report) |\n| Mermaid, math, tabs, flowchart, and include plugins | see \"Markdown extensions\" |\n\n**Styles:** v1's `.vuepress/styles/palette.styl` `$accentColor` (or **v2** `--vp-c-accent` in `.vuepress/styles/index.scss`) → `theme.accent`. Move other rules worth keeping from `index.styl` / `index.scss` to a root `theme.css`, written against Blume's tokens (report).\n\n## Navigation: `sidebar`\n\nThe rules in vitepress.md § \"Navigation: `sidebar`\" apply: multiple sidebars become tabs, a group whose pages sit flat beside other groups' pages becomes a **`(group)` folder** (`guide/(advanced)/frontmatter.md` still serves `/guide/frontmatter`, so no redirects; list it in the parent `meta.ts` `pages` by its bare name, `advanced`), and pages no sidebar lists need a decision. The item formats differ:\n\n- **Multiple sidebars** (`sidebar: { '/guide/': [...], '/': [...] }`, fallback key last) → one `navigation.tabs` entry per non-root key, as in VitePress. Items inside a key are relative to it: `''` is the key's `README.md`, and `'getting-started'` is `/guide/getting-started.html`.\n- **Items** are page paths, with `.md` optional, or `[path, 'Text']`. A `Text` that differs from the page's title → that page's `sidebar.label`.\n- **v1 groups** (`{ title, path, collapsable, sidebarDepth, children, initialOpenGroupIndex }`) → a folder (or `(group)` folder) with a `meta.ts` (`title`, `pages` in order). `path` → the folder's `index` page. `collapsable` unset or `true` → `display: \"group\"`. Blume's groups then start collapsed except the one holding the current page, as in v1 (which also opened the first group, or `initialOpenGroupIndex`, on a page no group held). When every group collapses, set `navigation.sidebar.display: \"group\"` once. `collapsable: false` → keep the default `flat`. `sidebarDepth` and `initialOpenGroupIndex` → drop.\n- **v2 groups** (`{ text, link, prefix, collapsible, children }`, where strings are file paths such as `'foo.md'` or `'/bar/README.md'`, resolved against `prefix`) → the same, except `collapsible: true` is the only collapsible case.\n- **`'auto'`** (v1, as a key's value or the whole `sidebar`) and **v2** `'heading'` (the default) showed only the current page's headings. Blume shows the page tree instead, with the headings in the outline rail. Report that the sidebar changed.\n- **Pages reachable only from the navbar** (common: `config/`, `faq/`, `miscellaneous/` with `sidebar: auto`) → give each section a place in the tree: a tab for a top-level navbar link, or a folder for a dropdown group. Use `sidebar.hidden: true` only for pages that were truly unlisted.\n\n## Navigation: `nav`\n\nMap it as in vitepress.md § \"Navigation: `nav`\". v1 items are `{ text, link }`, or `{ text, items }` for a dropdown; a dropdown can hold `{ text, items }` subgroups, which flatten. **v2** items are `{ text, link }`, `{ text, children, prefix }`, or a page path string. Strip `.md` and `.html` from internal `link`s, and turn `README.md` into its folder. `ariaLabel`, `target`, `rel`, and `activeMatch` → drop.\n\n**A dropdown whose pages span several folders** (VuePress's own \"Learn More\": `api/`, `faq/`, `miscellaneous/`, and an external changelog) fits no single tab. Make it a tab with `path: \"/\"` and `items` for the pages (on those routes the sidebar lists the folders no other tab claims, which here are exactly those, though the tab also highlights on the home page), and move its external links to `navigation.actions`. Or, for a few pages, use `navigation.featured`.\n\n## Titles\n\nVuePress names a page with its frontmatter `title`, falling back to a heading on the page's first line (normally the `# H1`). That name is the sidebar text and the `<title>`, while the H1 still renders as written. v1's `metaTitle` overrides the whole `<title>`. Blume renders frontmatter `title` as the H1. On every page:\n\n1. Move the H1 text into `title` and delete the H1 line. Keep only the text of an H1 that carries a link or a `<Badge>`: move the link into the body, and the badge to `sidebar.badge`.\n2. A frontmatter `title` that differs from the H1 → `sidebar.label`, since it was the sidebar text.\n3. `metaTitle` → `seo.title`, minus VuePress's ` | Site` suffix: Blume adds ` - <site title>` itself. A `metaTitle` without that suffix was the whole `<title>`: keep it as written and report that the site name is now appended.\n\nA page with no H1 and no `title` took the site title. Give it a real one. **VuePress gave the H1 an id, but Blume's title has none**, so a link to it (`using-a-plugin.html#using-a-plugin`, meaning the top of the page) fails `validate` with `BLUME_BROKEN_ANCHOR`: drop the fragment.\n\n## Markdown extensions\n\nEverything Blume renders from these is MDX-only unless vitepress.md marks it `.md` OK, so a page that needs a callout or component is renamed to `.mdx`.\n\n| VuePress | Blume |\n| --- | --- |\n| `::: tip` / `::: warning` / `::: danger` (v1 default theme). **v2** adds `info`, `note`, `important`, and `caution` | same as VitePress's containers, with the space removed and a title moved into `[Title]`. **v2** `important` → `:::note` (Blume's alias), `caution` → `:::danger`. v1 has no `note` or `info` container: unless a plugin registers one, the live site showed those lines as text, so converting them changes the page (report) |\n| `::: details Title` | same as VitePress: `<Expandable title=\"Title\">`, or `title=\"Details\"` when untitled. VuePress has no `{open}` |\n| `::: v-pre` | same as VitePress's `::: raw` row |\n| `::: slot name` (v1 Markdown slots) | no equivalent. Put the content where the layout showed it, or drop it (a home page's `::: slot footer` → see \"Home page\") (report) |\n| `<code-group>` + `<code-block title=\"YARN\">` (v1, 1.6+) | `<CodeGroup>` with blank lines inside, each block's `title` becoming its fence's title (` ```bash YARN `, or `title=\"Two words\"`). `active` on a later block → move that block first (Blume opens the first tab), or use `<Tabs defaultTabIndex={n}>` with one fence per `<Tab title>`. Inside a list item, indent `<CodeGroup>`, its fences, and the closing tag to the item's content column, or the list breaks. Unconverted, the blocks render stacked with no warning |\n| `<CodeGroup>` + `<CodeGroupItem title>`, or `:::: code-group` + `::: code-group-item Title` (**v2** betas) | the same. Unconverted in `.mdx`, `CodeGroupItem` fails the build, and the spaced containers show as text |\n| `::: code-tabs#id` + `@tab Label` / `@tab:active Label` (**v2** default theme, hope) | `<CodeGroup>`, each `@tab` label becoming the title of the fence that follows it (VuePress rendered only that first fence). `#id` → drop: Blume already syncs same-titled tabs across the page. `:active` → as for `<code-block active>` |\n| `::: tabs#id` + `@tab Label` (**v2**, hope) | `<Tabs>` of `<Tab title=\"Label\">`. `#id` → `syncKey=\"id\"`, `@tab:active` → `defaultTabIndex={n}` (zero-based), and `@tab Label#value` → keep only `Label` |\n| `:::: tabs` + `::: tab \"Label\"` (`vuepress-plugin-tabs`, v1) | `<Tabs>` of `<Tab title=\"Label\">` |\n| ` ```js{4} ` | same as VitePress: Blume reads it as written, and the codemod adds the space |\n| **v2** ` ```ts title=\"config.ts\" ` | works as written (`.md` OK) |\n| **v2** `:line-numbers`, `:line-numbers=N`, `:no-line-numbers` | the first two as in VitePress; `:no-line-numbers` → remove. **v2** turns line numbers on for every block by default; Blume has no site-wide switch (report). `:collapsed-lines`, `:v-pre`, `:no-v-pre` → remove |\n| **v2** `// [!code …]` (the `notation*` options) | same as VitePress |\n| `<<< @/path/file.js{2}` / `<<< @/file.js#region` (v1) | `<include>`, mapped as in VitePress's `<<<` rows, but **`@` is the directory `vuepress` ran in, not the source dir.** Running `vuepress build docs` from the repo root makes `@/docs/x.js` → `<include>/x.js</include>`. v1 imports take no `[label]` or `{lang}`. A region, or a file outside the content root → split or copy the file, as VitePress's rows say |\n| **v2** `@[code](./file.js)` / `@[code{3-10} js{2}:no-line-numbers](../file.js)` | `<include lang=\"js\" meta=\"{2}\">../file.js</include>`. Paths are relative to the page, like `<include>`'s. A line range (`{3-10}`) → split the file (report). Unconverted, it renders as `@` plus a link to the file, with no error |\n| **v2** `<!-- @include: ./part.md -->`, with `{start-end}` or `#region` (`@vuepress/plugin-markdown-include`, md-enhance, hope) | same as VitePress's `<!--@include:-->` row. Unconverted in `.md` it stays an HTML comment, so the content disappears silently. In `.mdx`, it fails the build |\n| `[[toc]]`, `:emoji:` | same as VitePress. VuePress left an emoji shortcode out of a heading's anchor (`## Emoji :tada:` → `#emoji`), and Blume's `## Emoji 🎉` is `#emoji` too, so the pasted emoji keeps the id |\n| `@flowstart` … `@flowend` (`vuepress-plugin-flowchart`) | renders as literal text. Redraw it as a ` ```mermaid ` flowchart (`.mdx`) or an image (report) |\n| **v2** `> [!NOTE]` GitHub alerts (with the hint plugin's `alert` on, or in hope) | same as VitePress's alerts row |\n| Math, ` ```mermaid `, markdown-it-attrs (`{#id}`, `{.class}`) | same as VitePress's rows. v1 and v2 core have no `{#id}` or attrs; only plugins (hope's `attrs`) add them |\n\n## Vue in Markdown\n\nvitepress.md § \"Vue in Markdown\" applies: the `.mdx` build errors, `{{ }}`, `<script>`/`<style>` blocks, comments, `<ClientOnly>`, directives, and the per-component decision. VuePress adds:\n\n- **Globals:** `$page`, `$site`, `$frontmatter`, `$themeConfig`, `$lang`, `$localePath`, `$title`, `$description`, and `$style` (from `<style module>`) → substitute or delete. `$withBase('/img.png')` → the plain root path (`![alt](/img.png)`): Blume adds `deployment.base` to internal links and assets.\n- **`<Badge>`** → Blume `<Badge>`, with the text as **children**. v1 `type`: `tip` (the default) or `green` → `accent`; `warning`, `warn`, or `yellow` → `warning`; `error` → `danger`. **v2** `type`s map as in vitepress.md. `vertical` → drop. An unconverted badge renders empty in `.mdx`, which warns `BLUME_UNKNOWN_PROP`, and as nothing in `.md`, with no warning. **Anchors:** VuePress left a badge out of a heading's anchor (`## Custom Containers <Badge text=\"default theme\"/>` → `#custom-containers`), and so does Blume, so the converted heading keeps its id.\n- **`<router-link to=\"/x.html\">`** (v1), `<RouterLink>`, and **v2** `<RouteLink>` → a Markdown link to the route (`[text](/x)`). `<OutboundLink/>` and `<Content/>` → delete.\n- **Global components:** v1 registers every `.vuepress/components/**/*.vue` file globally, named after the path (`Foo/Bar.vue` → `<Foo-Bar/>`, `demo-1.vue` → `<demo-1/>`), plus theme `global-components` and `Vue.component()` calls in `.vuepress/enhanceApp.js`. **v2** registers them through `@vuepress/plugin-register-components` or `.vuepress/client.{js,ts}`. Decide each as in VitePress. A hyphenated tag passes through as an unknown HTML element, so give the Blume component a PascalCase name and rename its tags (`<demo-1/>` → `<Demo1 />`). **v1 components are Vue 2**, but `@astrojs/vue` runs Vue 3, so an island needs a Vue 3 port: functional components, filters, `$listeners`, `.sync`, and `this.$set` all changed (report). A component that reads `$page`, `$site`, `$themeConfig`, or `this.$router` can't run outside VuePress: rewrite it or drop it.\n- **An escaped `\\<App>`** (VuePress's `api/node` headings) renders as text in `.mdx`, with no warning, so it can stay as written.\n- **`.vuepress/enhanceApp.js` / v2 `client.{js,ts}`** (Vue plugins, router hooks, global components) → drop, and report what each did. **v2** client-config `layouts` → custom `.astro` pages.\n\n## Home page (`home: true`)\n\nRebuild it as in vitepress.md § \"Home page (`layout: home`)\": `index.mdx` with `mode: custom`, or a designed `pages/index.astro`. It's usually the source dir's `README.md` (or `index.md`). VuePress's keys:\n\n- v1: `heroImage`, `heroAlt`, `heroText` (it defaults to the site title, and `null` hides it), `tagline` (it defaults to the site description, and `null` hides it), `actionText` + `actionLink` (one button), `features: [{ title, details }]`, and `footer`. **v2:** `actions: [{ text, link, type }]` replaces the single action, and adds `heroImageDark`, `heroHeight`, and `footerHtml` (which renders `footer` as HTML).\n- `mode: custom` is full-bleed with no padding, so a hero image renders huge and flush left. Wrap the hero in a centered block (`<div style={{ maxWidth: \"60rem\", margin: \"0 auto\", padding: \"3rem 1.5rem 0\", textAlign: \"center\" }}>`, with blank lines inside so the Markdown renders) and cap the image (`style={{ maxHeight: \"280px\", margin: \"0 auto\" }}`). Add `sidebar.hidden: true` so the home page isn't a sidebar row.\n- Features have no icon or link: each becomes a `<Card title>` with `details` as the body, in a `<CardGroup cols={3}>`. Features written as raw HTML in the body (`<div class=\"features\">`) map the same way.\n- `footer` (HTML with **v2** `footerHtml: true`) or a `::: slot footer` is usually a copyright or license line (\"MIT Licensed | Copyright © 2018-present …\") → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); move its links into `footer.links`, or they lose their URLs (report). A translated home page's `footer` → that locale's entry in a locale map. Other footer content → drop (report). `heroImageDark` → Blume has no light/dark pair for a content image, so keep one (report).\n\n## Frontmatter\n\nBlume's schema is strict: every key below must be mapped or removed.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `metaTitle` | see \"Titles\" |\n| `description` | pass through |\n| `meta` (v1) / **v2** `head` | per entry, as in vitepress.md's frontmatter `head` row |\n| `canonicalUrl` (v1.7.1+) | `seo.canonical` |\n| `lang` | remove: Blume sets `<html lang>` per locale |\n| `permalink` | `slug`, the old path without its leading `/`, trailing `/`, or `.html` (see \"URLs and redirects\") |\n| `layout` naming a `.vuepress/components` or theme component | `mode: custom`, and rebuild the content, or use a custom `.astro` page (report). `layout: Layout` → remove |\n| `home` and the hero keys | see \"Home page\" |\n| `sidebar: auto` / **v2** `sidebar: heading` | remove: the outline rail lists the headings |\n| `sidebar: false` | `mode: center` |\n| **v2** `sidebar` as an array (a per-page sidebar) | remove (report) |\n| `sidebarDepth` | remove |\n| `prev` / `next` | same as VitePress |\n| `search: false` (v1) | remove (report): it hid the search box on that page, which stayed searchable |\n| `tags` (v1, extra search terms) | `search.keywords` |\n| **v2** `date` | keep on `type: blog` pages, otherwise remove |\n| **v2** `redirectFrom` / `redirectTo` | `redirects` (see the redirect plugin row) |\n| `navbar`, `pageClass`, `editLink`, **v2** `editLinkPattern`, `lastUpdated`, `contributors`, `externalLinkIcon`, `permalinkPattern`, `routeMeta` | drop (report) |\n| keys a theme reads | see \"Themes\" |\n\n## Themes\n\n- **A theme that `extend`s `@vuepress/theme-default`** (v1's `@vuepress/theme-vue`, or a local `.vuepress/theme/index.js` with `extend`) → treat it as the default theme. Layouts it overrides (ads, banners) → drop (report).\n- **An ejected or local theme** (a full `.vuepress/theme/` with `Layout.vue`, `Navbar.vue`, …) → map the `themeConfig` keys it shares with the default theme. Any extra key (a version list inside the sidebar, say) is custom, so read its components to see what it rendered (report).\n- **`vuepress-theme-hope`** (`hopeTheme({…})`, often in `.vuepress/theme.ts`):\n  - **Sidebar** `\"structure\"` builds the tree from files, which Blume does by default. `order` → `sidebar.order`, `index: false` → `sidebar.hidden: true`, and `shortTitle` → `sidebar.label`. Hope sorts positive `order`s first, then unordered pages by title, then negative `order`s (`-1` last), while Blume sorts a negative order first: renumber, or list the folder's order in its `meta.ts` `pages`. A folder `README.md`'s `dir` → that folder's `meta.ts`: `text` → `title`, `icon`, `order`, `collapsible` (on by default) → `display: \"group\"`, and `expanded: true` → `collapsed: false`. `dir.index: false` (the folder left out of the generated sidebar) → `sidebar.hidden: true` on each of its pages.\n  - **Icons** (frontmatter, navbar, sidebar) are FontAwesome or Iconify names → Lucide (SKILL.md \"Icons are Lucide\").\n  - **Markdown** (the theme's `markdown` options, or `plugins.mdEnhance`): `^sup^`, `~sub~`, task lists, and footnotes work as written. `==mark==` → `<mark>…</mark>`. `::: center` / `::: right` → drop the alignment. `!!spoiler!!` → drop. `<VPCard>` → `<Card>`. A ` ```component Name ` fence (YAML or JSON props) → that component with the props as attributes. `<Catalog />` → `directory: \"card\"` in that folder's `meta.ts`. `<!-- more -->` → delete. Charts (Chart.js, ECharts, Markmap, PlantUML, flowchart), playgrounds, `::: demo`, and slides → no equivalent: a mermaid diagram, an image, or a link (report).\n  - **Frontmatter:** `author` and `date` → `authors` and `date` on `type: blog` pages, otherwise drop. `category` and `tag` → drop (report: Blume has no tag pages), or `search.keywords`. `article`, `star`, `sticky`, `cover`, `isOriginal`, `pageInfo`, `breadcrumb`, `toc`, `comment`, `copy`, `footer`, `copyright`, `containerClass`, and any other hope-only key (`license`, `pageview`, `backToTop`, `sitemap`, `feed`, …) → drop. The project home's `highlights` → more cards. `bgImage` and the hero style keys → drop (report).\n\n## Icons\n\nThe default theme has no page icons. Icons you add to tabs, `meta.ts`, cards, or `sidebar.icon` are kebab-case Lucide names. Hope's icons map as above.\n\n## Assets\n\n- **`.vuepress/public/`** → `public/`. Root-absolute references (`/hero.png`) keep working.\n- **Relative images** (`./img.png`) work as written.\n- **Webpack paths** (v1 `~@alias/img.png`, `~some-package/img.png`, the `@source` alias) → copy the file into the content root or `public/` and link it plainly.\n\n## i18n\n\nMap it as in vitepress.md § \"i18n\". VuePress's `locales` keys are paths (`'/zh/'`), so the `code` is the folder (`zh`), not the `lang` (`zh-CN`). The default locale's `lang` (`en-US`) gives `defaultLocale` (`en`). The theme's per-locale options (v1 `themeConfig.locales`, **v2** the theme's `locales`) carry the rest:\n\n- `label` (**v2** `selectLanguageName`) → that locale's `label`. `selectText` (**v2** `selectLanguageText`), `ariaLabel`, `editLinkText`, the `lastUpdated` text, `serviceWorker`, and the other UI labels → drop, or `i18n.ui`.\n- `nav` → per-locale label maps on `navigation.tabs` / `actions`.\n- `sidebar` (keyed `'/zh/guide/'`, …) → each locale's own folder `meta.ts` files.\n- A per-locale container title (`defaultTitle: { '/zh/': '提示' }`) → drop: Blume's UI packs (including `zh`) title callouts.\n- A per-locale `title` / `description` → drop; a translated home page can keep its description in frontmatter.\n- **Anchors:** keep each locale's own old ids (`/zh/` pages had Chinese slugs, which zh pages and outside zh links use). The anchor script does this, pairing every page with its own old page. vitepress.md's \"Pin translated headings\" means exactly that, not pinning to the default locale's ids.\n\n## URLs and redirects\n\nFollow vitepress.md § \"URLs and redirects\", treating VuePress like `cleanUrls: false`: one exact redirect per non-index `.html` page, never a redirect from an index URL, both spellings for a moved route, and rewritten internal links. VuePress specifics:\n\n- **Build the old site first and keep its output** (`<source>/.vuepress/dist`, or `dest`): it's the URL list and the record of the old anchors. VuePress writes no sitemap unless a plugin adds one. v1 runs on webpack 4, which fails on current Node with `error:0308010C:digital envelope routines::unsupported`: build it with `NODE_OPTIONS=--openssl-legacy-provider`, or under Node 16 (`npx -p node@16 …`).\n- **Redirects:** run this in that output directory and paste the result into `redirects` (the VitePress codemod's `--redirects` reads only VitePress builds):\n\n  ```bash\n  find . -name '*.html' ! -name index.html ! -name 404.html | sed 's#^\\.##' | sort \\\n    | awk '{ to = $0; sub(/\\.html$/, \"\", to); printf \"  { from: \\\"%s\\\", to: \\\"%s\\\" },\\n\", $0, to }'\n  ```\n\n- **`README.md` → `index.md`** keeps the folder URL (`/guide/` is served from `guide/index.html`), so it needs no redirect. Rewrite each link to a `README.md` to its folder (`./README.md` → `./`, `/guide/README.md` → `/guide`). Once the file is renamed, `blume validate` flags the ones you miss.\n- **`permalink` pages** served the permalink, not the file path. Set `slug` to it. A permalink ending in `.html` also needs a redirect from that `.html` URL.\n- **Links:** VuePress authors link to `.md` files, relative or absolute (`/guide/foo.md`; **v2** recommends relative), and to `.html`. Rewrite them as vitepress.md says. Also fix links in `nav`, `sidebar`, `actionLink`, hero `actions`, and raw `<a href>`.\n- **Anchors: pin them with the bundled script.** VuePress slugs headings as VitePress does, so ids change on many pages (115 headings on 32 of 92 pages in VuePress's own docs, counted before Blume left badges and trailing emoji out of its ids). **Duplicate headings number differently:** VuePress's second `## Options` is `#options-2`, Blume's is `#options-1`. After `blume build`, run from the Blume project:\n\n  ```bash\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist>          # dry run\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist> --write  # apply\n  ```\n\n  It pairs each page's headings with the old build's by text and position, translated pages with their own old pages, and appends `[#old-id]` (`{#old-id}` in `.md`) where the ids differ. Rebuild and rerun until it reports 0. A `NOTE` names a heading whose source line it couldn't match, such as one with inline code, a Markdown escape (`\\<`), or `--` (19 of the 115 in VuePress's docs): pin those by hand. It also pins the home hero's `#main-title`, which is harmless. On a large site, `--only-linked` pins only the ids something links to.\n\n- **`validate` also reports anchors that were already broken.** VuePress never checked fragments (a zh page linking an English id, a renamed section). Point each at the right heading or drop the fragment, and report it.\n- **Hosts:** Netlify (with pretty URLs) and GitHub Pages also answered `/guide/foo` without `.html`, which Blume keeps. On Netlify, a static build for no named host writes unforced `_redirects` rules, so Netlify serves the meta-refresh page instead of a 301: use `deployment: netlify({ output: \"static\" })` (see Redirects in `docs/02-deployment.mdx`). The GitHub Pages caveat in vitepress.md applies.\n\n## Teardown\n\n- Remove `vuepress`, `vuepress-vite` / `vuepress-webpack`, `@vuepress/*`, `vuepress-plugin-*`, `vuepress-theme-*`, `vue`, `vue-template-compiler`, `vue-server-renderer`, and any markdown-it, Stylus, Sass, or webpack packages that only served VuePress. Keep `vue` with `@astrojs/vue` if you made Vue islands.\n- Delete `.vuepress/` (config, `config/`, `components/`, `theme/`, `styles/`, `enhanceApp.js` / `client.*`, `dist/`, `.temp/`, `.cache/`) once `public/` has moved.\n- Repoint the scripts (`docs:dev`, `docs:build`) to `blume dev` / `blume build` / `blume preview`, and add `blume`. Remove `NODE_OPTIONS=--openssl-legacy-provider`.\n- **Node:** Blume needs 22.19 or later, and v1 sites often pin an old one (`.nvmrc`, `engines`, CI `node-version: '14'`, Netlify's `NODE_VERSION`). Bump each. When the same CI job also builds or tests other packages (yarn 1 refuses to install a dependency whose `engines` excludes the running Node), the bump reaches them too, so report it.\n- Grep the repo for `.vuepress/dist`, `dest`, and `vuepress build`. Deploy scripts (the VuePress guide's `deploy.sh` pushing `docs/.vuepress/dist` to `gh-pages`), `netlify.toml` publish dirs, and CI workflows must now publish Blume's `dist/`. Settings kept in a host's dashboard (Netlify's publish directory, build command, and `NODE_VERSION`) can't be committed: list them for the user, with a full (not shallow) clone if you set `lastModified: \"git\"`.\n- `.gitignore`: replace the docs' own entries (`.vuepress/dist`, the old `dest`) with `.blume` and `dist`. Keep entries other packages in the repo still use (`.temp`, `.cache`).\n- A `404.md` → delete it (as in VitePress).\n\n## Dropped — report these\n\nEverything vitepress.md's list covers that this site used, plus: v1 Markdown slots, `@flowstart` diagrams that weren't redrawn, the PWA and service worker, `sidebarDepth` and heading-only (`auto`) sidebars, `nextLinks` / `prevLinks` when only one was off, v1 `<code-block active>` defaults that couldn't move, Vue 2 components that weren't ported, `enhanceApp` / client-config code, `.vue` pages, `pageClass` and layout components, Stylus/Sass palette overrides, Universal Analytics ids, the DocSearch index, GitLab and Bitbucket edit links, old-site anchors that were already broken, hope-only components and frontmatter, and any `note` / `info` text that v1 showed literally and now renders as a callout.\n","isInternal":false,"tokens":11036,"sizeBytes":39248},{"name":"SKILL.md","path":".agents/skills/blume-migrate/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-migrate/SKILL.md","title":"blume-migrate","category":"anthropic-skill","format":"markdown","content":"---\nname: blume-migrate\ndescription: Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, VitePress, VuePress, Docus, MkDocs, mdBook, Fern, GitBook, ReadMe, Redocly, Docsify, Jekyll (Just the Docs), GitHub Wiki, or any docs framework) to Blume, the markdown-first docs framework on Astro. Translate the source config to blume.config.ts, restructure content into Blume's filesystem-derived navigation, rewrite JSX callouts to directives, convert icons to Lucide, and inline snippets. Use when the user asks to migrate/convert/port a docs repo to Blume, or when the repo has a docs.json/mint.json, docusaurus.config.*, meta.json with fumadocs, _meta.* with nextra, an astro.config.* with starlight(), a .vitepress/config.* or .vuepress/config.*, an mkdocs.yml or zensical.toml, a book.toml (mdBook), a fern/docs.yml, a GitBook .gitbook.yaml/gitbook-docs.yaml, ReadMe _order.yaml files in docs/ or reference/, a Redocly sidebars.yaml or @redocly/realm dependency, an index.html that sets window.$docsify, a docus or @nuxt-themes/docus dependency, a Jekyll _config.yml (Just the Docs theme), or a GitHub wiki clone (Home.md with _Sidebar.md, [[Page Name]] links).\n---\n\n# Migrate to Blume\n\nBlume is a **markdown-first** documentation framework on Astro/Vite. You drop Markdown/MDX into a folder and get navigation, search, theming, Open Graph images, and a component library with no app boilerplate — **the framework is the template**. There is no starter to clone; the only thing a project owns is its content and a `blume.config.ts`.\n\nYour job is to convert a source docs repo into an **idiomatic** Blume project — not a 1:1 transliteration. Read this file, detect the source framework, open the matching `references/<framework>.md` for the exact mappings, and work the loop below. Report everything you drop or approximate.\n\nThroughout this skill (including the `references/` files), **`<skill>` means the absolute path of the directory containing this SKILL.md** — resolve it from wherever you read this file (e.g. `node_modules/blume/skills/blume-migrate` or `.claude/skills/blume-migrate`). It is a placeholder to substitute, never a literal path.\n\n## Migration philosophy\n\n- **Target idiomatic Blume, not a mechanical port.** Prefer filesystem-derived navigation over an exhaustive explicit `navigation.sidebar`. Prefer `:::` directives over JSX callouts. Prefer Blume defaults over restating them in config.\n- **Every field has a default; `{}` is a valid config.** Map only what the source _declares_. If the source uses a framework default, don't write it.\n- **Drop chrome that has no Blume equivalent — and say so.** Navbar CTAs, custom theming, conditional redirects, and unmappable icons get reported to the user, not silently discarded or faked.\n- **Convert, don't preserve.** Blume's page frontmatter schema is **strict** — unknown keys are build errors. A source-only frontmatter key must be mapped to a Blume key or removed (and reported), never left to \"maybe validate.\"\n\n## Migration workflow\n\n1. **Detect the source framework** and read its reference file:\n   - `docs.json` / `mint.json` → **Mintlify** (`references/mintlify.md`) — the deepest, config-declared nav.\n   - `docusaurus.config.*` → **Docusaurus** (`references/docusaurus.md`).\n   - `meta.json` + `fumadocs-*` deps (content under `content/docs/`) → **Fumadocs** (`references/fumadocs.md`).\n   - `_meta.{js,ts,json}` + `nextra` deps → **Nextra** (`references/nextra.md`).\n   - `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).\n   - `.vitepress/config.{js,ts,mjs,mts}` (often `docs/.vitepress/`) or a `vitepress` dependency → **VitePress** (`references/vitepress.md`). `.vuepress/` is VuePress (next bullet).\n   - `.vuepress/config.{js,ts,mjs}` (v1 also `config.yml`/`config.toml`; often `docs/.vuepress/`), a `vuepress.config.*`, or a `vuepress` dependency → **VuePress** (`references/vuepress.md`). It builds on `references/vitepress.md` for the Markdown layer the two share, so read both.\n   - A `docus` dependency (often in `docs/package.json` beside a `nuxt.config.ts` with `extends: ['docus']`), or `@nuxt-themes/docus` for 1.x → **Docus** (`references/docus.md`). Pages are MDC, Nuxt Content's component syntax: `::note` … `::` is not a Blume directive, and Blume prints it as text (`BLUME_MDC_SYNTAX` warns at its line).\n   - `mkdocs.yml`/`mkdocs.yaml` (also Zensical's `zensical.toml` and ProperDocs' `properdocs.yml`) → **MkDocs / Material for MkDocs** (`references/mkdocs.md`): nav declared in config, Python-Markdown extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets), and Python deps, so usually no `package.json`. Run `node <skill>/scripts/mkdocs-codemod.mjs --write <content-dir>` first, then `pin-heading-ids.mjs` after the first build.\n   - `fern/docs.yml` (beside `fern/fern.config.json`) → **Fern** (`references/fern.md`). A `fern/` folder without `docs.yml` only generates SDKs: the docs are elsewhere.\n   - `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, or a `SUMMARY.md` beside a `.gitbook/` folder → **GitBook** (`references/gitbook.md`). `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit (same reference, its own section); `book.toml` + `src/SUMMARY.md` is mdBook (`references/mdbook.md`).\n   - `book.toml` (at the book root, often `docs/`, `book/`, or `guide/` in a code repo; chapters in its `[book] src`, beside a `SUMMARY.md`) → **mdBook** (`references/mdbook.md`). Every URL ends in `.html`, `SUMMARY.md` is the whole nav, and `{{#include}}` pulls anchored code from across the repo: build the old book first, since it's the URL list, the old ids, and the only record of what the includes showed.\n   - `sidebars.yaml` or an `@redocly/realm`-family dependency, usually beside a `redocly.yaml` and with Markdoc `{% … %}` tags in pages → **Redocly** (`references/redocly.md`). GitBook pages use `{% … %}` tags too, so check for GitBook's files first. A `redocly.yaml` holding only lint/bundle keys (`extends`, `rules`, `decorators`, `apis`) is Redocly CLI config, not a docs site: keep it and detect the framework from the other signals.\n   - `_order.yaml` files in `docs/`, `reference/`, `recipes/`, or `custom_pages/` (ReadMe's bi-directional Git sync), or a CI step running `readmeio/rdme` beside pages with `category`/`parentDoc` (or `category.uri`/`parent.uri`) frontmatter (`rdme` uploads) → **ReadMe** (`references/readme.md`). ReadMe keeps settings, redirects, the glossary, and variables in its dashboard, not the repo, and changelogs only on the `main` branch.\n   - An `index.html` setting `window.$docsify` (usually in `docs/`, beside `_sidebar.md`), or `docsify-cli` in `package.json` → **Docsify** (`references/docsify.md`). Run `node <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old --group-folders <docs-dir>` first, then `pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json` after the first build. Its default URLs live after `#` (`/#/page`), which no server-side redirect can match; the reference adds a client-side script.\n   - `_config.yml` (root or `docs/`) → **Jekyll** (`references/jekyll.md`); Just the Docs is `just-the-docs` in `_config.yml` or the `Gemfile`. Build the old site first with a project-local bundle: the nav comes from front matter (`parent`/`nav_order`) and is rebuilt from the old build's sidebar, and URLs end in `/` or `.html`. A `_toc.yml` beside it is Jupyter Book, not Jekyll. Run `node <skill>/scripts/jekyll-codemod.mjs --old <old build> --write .` from the Jekyll source dir first, then `pin-heading-ids.mjs --old <old build>` after the first build.\n   - A clone of `<repo>.wiki.git` (`Home.md` at the root, often `_Sidebar.md` and `_Footer.md`, `[[Page Name]]` links, no config) → **GitHub Wiki** (`references/github-wiki.md`). Never detected: the source is named. Its old URLs stay on github.com, which can't redirect, so the reference writes a route table and link stubs for the old wiki instead of `redirects`. Run `node <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write` first, classify `wiki-routes.json`, then `convert --repo <owner>/<repo> --write`.\n   - Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.\n   - **Also note the host repo, independent of source framework:** a pnpm/Turbo workspace, a non-`docs/` content layout, or a Vercel deploy each need integration steps (`content.root` scoping, `minimumReleaseAge`, lockfile, `vercel.json`, an Astro/Vite patch) — all in `references/monorepo.md`. Read it whenever the target isn't a bare single-package docs folder.\n2. **Inventory the repo** before changing anything (a page whose body is only a `<meta http-equiv=\"refresh\">` is a redirect, not a page: add the redirect from its old URL and report it): the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs and GraphQL schemas, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted. Pages the old site published by accident (a `.github/` template, files under dot-folders, planning notes) aren't content: leave them out, add no redirects, and list them in the report.\n3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: \"…\" })`.\n4. **Restructure content.** Put `blume.config.ts` in the directory whose `package.json` runs the old docs build, which may be a workspace package such as `packages/docs` rather than the repo root. Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: \".\"` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** A group that exists only in the source's nav config, with its pages flat on disk, becomes a **`(group)/` folder**: it adds the sidebar group and keeps every URL, with no redirects (moving pages a folder deeper instead changes their URLs and breaks their relative asset paths). Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.\n5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; turn snippets/partials into `<include>` statements or inline them (Blume has no import-based includes: `import Snippet from \"…\"` plus `<Snippet />` has to become one or the other; when a source includes part of a file (mdBook `ANCHOR`s, VitePress `#region`s, MkDocs snippet line ranges) or a file outside the content root, copy `<skill>/scripts/include-excerpts.mjs` into the project and run it before every `dev`, `build`, and `validate` rather than pasting code the project compiles: it reports every missing anchor or region in one pass, and a restored marker in the source beats a line range, which goes stale silently); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources — and links inside OpenAPI descriptions, which Blume renders and validates, though a redirect `from` hides a stale one; rewrite links that hardcode the old site's own origin to root-relative paths); **add a `redirects` entry for every route you moved** in step 4 (a hash-routed source like Docsify also needs its reference's client-side redirect, because `redirects` only see paths); **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`), and fix links to its anchor (`#<h1-slug>`), which no longer exists: drop the fragment. `blume validate --strict` also reports fragments the old site never checked; fix each or drop it, and report it. A page with no frontmatter `title` takes its opening `# H1` as the title and shows it once, so that one can stay; moving it into `title` is still the idiomatic form. **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `githubReleases()` source** (see \"Changelogs\" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`). For **Docus**, run the bundled codemod first: `node <skill>/scripts/docus-codemod.mjs --write content` pairs MDC blocks as remark-mdc does, converts the Nuxt UI prose components, fence labels, icons, frontmatter, and `.navigation.yml` files, makes the MDX prose safe, and lists every `REVIEW` item (custom and landing-page components stay as written; check them against the live site, not only a local build; see `references/docus.md`). For **VitePress**, run the bundled codemod first: `node <skill>/scripts/vitepress-codemod.mjs --write <srcDir>` converts containers (paired as markdown-it pairs them), code groups, alerts, fence labels, `<<<` imports and includes, `.html` links, badges, and H1 titles, and lists every `REVIEW` item for you to finish (see `references/vitepress.md`). Before converting any source's components, check its built output: a component its theme never registered rendered nothing there (`<!---->`), so converting it shows content readers never saw; report it. For **mdBook**, convert every `{{#…}}` helper before anything else: anchored, line-range, and out-of-root code becomes generated excerpts, one-line files become `variables`; then strip hidden `# ` lines and `rust,…` fence attributes, and convert admonish blocks (see `references/mdbook.md`). For **Jekyll**, convert every Kramdown IAL (`{: .note }`) and Liquid tag: in `.md` they ship as literal text (a Liquid tag warns `BLUME_TEMPLATE_TAG`, an IAL `BLUME_MD_ATTRIBUTE_LIST`), and in `.mdx` they fail the build (`blume check` names each IAL's line as `BLUME_MDX_ATTRIBUTE_LIST` and a Liquid tag's as `BLUME_MDX_SYNTAX`, at the partial's own line for one in an included partial). Run the bundled codemod first: it converts callout IALs, the in-page TOC, H1s, front matter, Markdown includes, and Liquid links and variables, rebuilds the Just the Docs sidebar as folders, `slug`s, and `meta.ts`, and writes the redirects and variables to `jekyll-migration.json` (see `references/jekyll.md`). For a **GitHub Wiki**, run the bundled codemod (`routes`, then `convert`): it builds the route table (one flat route per page name) and converts every `[[…]]` link outside code, which Blume would otherwise render as literal text (`BLUME_WIKILINK_UNSUPPORTED` warns only for one that names a page or has a `|`; Markdown pages put the link text first: `[[text|Page]]`). For **Redocly**, convert every Markdoc `{% … %}` tag: Blume never parses them, so an unconverted tag ships as literal text in `.md` (`BLUME_TEMPLATE_TAG` warns at its line) and fails an `.mdx` build. For **VuePress**, rename every `README.md` to `index` first, then run the VitePress codemod (its limits on VuePress content are in `references/vuepress.md`), then grep for hyphenated theme tags (`<code-group>`, `<code-block>`, `<router-link>`): they pass through `.mdx` as unknown HTML elements with no diagnostic. For **MkDocs**, decide `.md` vs `.mdx` per page before converting: MDX rejects HTML comments, `<https://…>` autolinks, unclosed void tags, and `{…}` attribute lists that Python-Markdown accepts (`blume check` names each at its line: `BLUME_MDX_SYNTAX` for what MDX can't parse, and the last two as `BLUME_MDX_UNCLOSED_ELEMENT` and `BLUME_MDX_ATTRIBUTE_LIST`), and snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), not the page (see `references/mkdocs.md`). For **Fern**, URLs come from `docs.yml`, not file paths: run `node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt` before moving anything (dry run, then `--write`), and after the first build its `endpoints` step, which joins `operation-routes.mjs` output to Fern's old API URLs and writes `fern-redirects.json`; export a Fern Definition to OpenAPI with `fern export` (see `references/fern.md`). For **GitBook**, put each page at its old URL first (GitBook's URLs follow `SUMMARY.md`, not the file tree), then convert every `{% … %}` tag: in `.md` an unconverted tag ships as literal text (`BLUME_TEMPLATE_TAG` warns), and in `.mdx` it fails the build. For **ReadMe**, run the bundled codemod first: `node <skill>/scripts/readme-codemod.mjs --write --site <hub origin> --glossary <saved hub page> .` keeps every page at its flat URL, converts callouts, `doc:`/`ref:` links (`blume validate` names any left as `BLUME_UNSUPPORTED_LINK_SCHEME`), ReadMe components, and frontmatter, writes `meta.ts`, overlays, and redirects, and lists what needs a person; after the first build, its `--routes` pass adds the endpoint redirects and per-tag order (see `references/readme.md`). A pasted widget's JSX event handler (`onClick={…}`) builds green and never runs on a static page: `BLUME_MDX_EVENT_HANDLER` names each one, so move the widget into an island (`islands/`) or drop it and report it.\n6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. Yarn's `npmMinimalAgeGate` refuses a fresh `blume` the same way: add it to `npmPreapprovedPackages`. **Always regenerate the lockfile in the same change** with the repo's own package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, or `bun install`; check `packageManager` and the lockfile's name) and commit it alongside `package.json` — CI and hosts install from a frozen lockfile, so a stale one fails the build before it starts. After creating the first new file in the content root (a `meta.ts`, a partial), run `git check-ignore -v <content root>/meta.ts`: a repo `.gitignore` rule that covers the content root leaves every new file out of `git add` while the old pages stay tracked, so fix the rule or report that each new file needs `git add -f`. Blume needs **Node 22.19 or later**: update `.nvmrc`/`.node-version`, `engines.node`, and any CI `node-version` in the same change. pnpm 11 and later run a dependency's build script only once `pnpm-workspace.yaml` approves it: add `allowBuilds: { esbuild: true }` (plus `\"@scarf/scarf\": false`, as `blume init` writes) unless the workspace already lists `esbuild`, keeping its other entries. Add `.blume/`, `dist/`, and `.env.local` to the docs package's own `.gitignore` when it has one, else the root's (as `blume init` does; a `vercel()` build appends `.vercel/` itself), and remove any line that ignores `public/` before moving assets into it. **`blume build` empties `<project>/dist`:** if the host repo already uses `dist/` for something else (Python wheels, a library build), put the Blume project in its own folder (`docs/`). If the repo formats with Prettier, extend its `*.md` overrides (`.prettierrc`, `.editorconfig`) to `*.mdx` and keep `{/* */}` comments on one line. After renaming or moving pages, grep the whole repo (release config, CI, bot and ignore files) for their old paths. Check any `tsconfig.json` in the Blume project dir too: Blume's build reads it, so an `extends` that resolves only after a root workspace install, or `include`s of the old framework's folders, fail `blume build`. Install the workspace first, fix the paths, or delete the file if only the old framework used it (a Nuxt `tsconfig.json` that `extends` or `references` `./.nuxt/*` fails with `BLUME_TSCONFIG_EXTENDS`, and a `postinstall: nuxt prepare` script goes with the Nuxt dependency). **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.71.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.\n7. **Wire up the host repo & deploy (non-trivial repos).** Settings a host keeps in its dashboard (Netlify's publish directory, build command, `NODE_VERSION`) can't be committed: list them in the summary. In `.gitignore`, replace only the docs' own entries and keep ones other packages use. For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`: with `outputDirectory: \"dist\"` for a static build, and with no `outputDirectory` for `deployment: vercel()`, whose build writes `.vercel/output` (add `.vercel/output/**` to Turborepo outputs). Tell the user the settings you can't commit (Vercel Root Directory, Node 22, and server secrets such as `AI_GATEWAY_API_KEY`). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.\n8. **Verify.** Run `blume build` (frontmatter schema, duplicate routes, config — it fails on any error diagnostic by default; **never pass `--no-strict`**, which builds anyway and silently drops pages with invalid front matter or `.mdx` that doesn't parse) and `blume validate --strict` (internal links, heading anchors, assets, redirect targets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).\n\n## The Blume mental model\n\nThe single biggest shift for most sources — especially Mintlify — is that **navigation is derived from the filesystem**, not declared in config.\n\n### Navigation is the file tree\n\n- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.\n- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).\n- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display, directory })`). The `pages` array lists children by slug (numeric prefix, parentheses, and extension stripped; an entry that names nothing warns `BLUME_META_UNKNOWN_PAGE` with the slug it likely meant, so write that one); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order. `directory` inherits down the tree: set `directory: \"none\"` on nested folders that shouldn't list cards. `pages` can't interleave loose pages with subgroups inside a `flat` group, or at the top of the sidebar in any mode: when a source interleaves them (mdBook, MkDocs `navigation.sections`), use `display: \"group\"`, where the order holds inside groups, and say what moved at the top level.\n- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `\"flat\"` (default), `\"group\"` (collapsible), or `\"page\"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).\n- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).\n- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting, **use a `(group)/` folder first**: move the pages into `<folder>/(<group>)/` with a `meta.ts` for the label, order, and `collapsed`. A parenthesized folder adds the sidebar group but no URL segment, so no route changes and no redirects are needed, and a page from another folder can join with a `slug` that keeps its old route. A group whose landing page is a sibling file keeps every URL too: move the landing page to `(group)/index.md` with `slug: <its old route>`, and the slugged index becomes the group's link. Moving a page a folder deeper breaks its relative links and assets (`./img.png`, `../other.md`), so re-base them. (Docsify is the exception: its old `#/` URLs are remapped by script whatever the paths, so its reference moves pages into real folders.) Only when that can't express the shape, move the pages into a real subfolder (URLs change: add `redirects`) or declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.\n\n### Tabs and selectors\n\n- **`navigation.tabs`** (`{ label, path, icon? }`) render top-of-header sections and **scope the sidebar by route** — the folder at a tab's `path` becomes the section, so this needs no config beyond the tabs themselves; structure content as **one folder per tab**. **A source's top-level tabs (Mintlify `navigation.tabs`, a top-level product/section switcher) map to these header tabs — keep them as tabs; don't flatten them into a single global `navigation.sidebar`.** Blume picks the active tab by **URL prefix** (longest tab `path` that prefixes the route), so every page in a tab must live under that tab's single `path`; a source tab that mixes arbitrary routes isn't portable as-is — either move its pages under one prefix (route change → add `redirects`) or accept the closest shape, and say which in the report (details in `references/mintlify.md`). The filtering runs both ways: on a route **under** a tab's `path`, the sidebar shows **only** that tab's folder (a tab also highlights when the current route is under it); on a **root or untabbed** route (or a tab whose `path` is `/`), the tab folders are **hidden** and the sidebar shows only the loose pages that belong to no tab (full tree as a fallback, so it's never blank). Consequence for migrations: once you add tabs, the landing sidebar automatically drops the sectioned content — that's intended, not lost pages; don't hand-build excludes for it.\n- **`navigation.selectors`** (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`, `kind` = `dropdown`/`product`/`version`/`language`) partition a whole site (products, versions) via a header dropdown keyed on the current route.\n- **`navigation.actions`** (`[{ label, href }]`) puts plain links in the **header**, left of the icon buttons, and **`navigation.cta`** (`{ label, href }`, singular) is the header's one filled call-to-action button. This is the home for a source's header bar — a \"Log in\"/\"Status\" link goes in `actions`, a \"Sign up\"/\"Get started\" button in `cta`. An `http(s)` href opens in a new tab; an internal route is validated against your pages at build time, so a route served by another app on the same host must be written as an absolute URL. Both hide on phones (below `sm`), except `cta` on a page with no navigation toggle; a link that must survive on a phone on docs pages belongs in `featured`.\n- **`navigation.featured`** (`{ label, href, icon? }`) pins links to the **top of the sidebar, above every section** — a blog, changelog, or support page that should always be one click away. These are the **exception to tab scoping**: unlike the generated tree, featured links show on **every** route and breakpoint. `href` points anywhere — an external URL opens in a new tab, an internal route (`/contact`) is validated against your pages at build time. `icon` is a Lucide name (or image path/URL/inline SVG), as everywhere else. This is the home for a source's always-visible header/utility links (Mintlify anchors, Blog/Contact links) — see `references/mintlify.md`.\n\n### Routes and pathing\n\n- A route is the content path relative to `content.root`, with **numeric prefixes stripped** (`01-intro.mdx` → `/intro`; a version or a name that starts with a date, `YYYY-MM-DD`, `D-M-YYYY`/`M-D-YYYY`, or `YYYY-MM`, stays whole: `12-05-2022.mdx` → `/12-05-2022`; pin `slug` for any other digit-led name the old site kept whole) and **`(group)/` folders adding no segment**. An `index` file maps to its folder's route. Frontmatter `slug` overrides the generated route. A `README.md` is an ordinary page at `/README`, not its folder's index: rename it `index.md` (Docsify, MkDocs, GitBook, and VuePress sources all use README as the index).\n\n### `blume.config.ts` shape\n\n`defineConfig({...})` — every field optional, all with defaults:\n\n- **Site:** `title`, `description`, `logo` (string SVG, or `{ image: string | { light, dark, alt }, text, href }`), `banner` (a string, or `{ content, link: { href, text }, dismissible, id }` — no color/type). A logo renders beside `title` in the header, so a **wordmark logo doubles the brand** (\"Acme Acme\") — set `text: \"\"` to render the mark alone. **Prefer the string form over `{ light, dark }`:** if you have the logo SVG locally and it's monochrome (solid black or white), rewrite its `fill`/`stroke` to `currentColor` and use `logo: \"/logo.svg\"` — it then inherits the theme's text color and adapts to light/dark automatically, so you don't need separate light/dark files.\n- **`theme`:** `accent` (a color string for both modes, or `{ light, dark }` per mode), `action` (color), `mode` (`light`/`dark`/`system`), `radius`, `fonts` (`{ body, display, mono }` — each a curated Google-font slug, a `{ name, provider?, weights? }` object for any Google/Fontsource/Bunny/Fontshare family, or `{ name, variants: [{ src, weight?, style? }] }` for local font files), `background` and `backgroundImage` (each a string, or `{ light, dark }` per mode). The old `accentDark`/`backgroundDark`/`backgroundImageDark` fields were **merged into these per-mode objects** — a bare string still applies to both modes, so only reach for `{ light, dark }` when the two modes differ. Each color must be a CSS color or an accent preset (a source's bare `\"3b82f6\"` needs its `#`), or `BLUME_THEME_COLOR_INVALID` warns and browsers ignore it. There is **no** `theme.strict` and **no** `theme.css` config field — custom CSS goes in a project-root **`theme.css` file** (auto-picked-up), and a source's \"strict appearance\" flags drop.\n- **`content`:** `root` (default `\"docs\"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `[\"**/*.{md,mdx}\"]` / `[\"**/_*\", \"**/.*\"]` — a set `exclude` **adds to** those defaults, so partials and dot-folders stay unpublished; to publish `_`-prefixed or dot-files on purpose, list the default with a `!`: `exclude: [\"!**/_*\"]`), `sources` (an array of **adapters imported from `blume/sources`**: `filesystem({ root, include, exclude })`, `obsidian({ vault })`, `githubReleases({ owner, repo })`, `notion({ database })`, `sanity({ projectId, dataset, query })`, `contentful({ space, contentType })`, `payload({ url, collection })`, `strapi({ url, contentType })`, `mdxRemote({ github })`, `custom(source)`; every factory with an options object also takes `prefix` and `pollInterval`, while `custom(source)` takes a `ContentSource` instance that sets its own `prefix`. The 1.x `{ type: \"…\" }` objects were removed — rename `type` to the factory call and pass the other fields as its options — except `{ type: \"custom\", source }`, which becomes `custom(source)` with the instance as the only argument. `root`/`include`/`exclude` are shorthand for a single `filesystem()` and are **rejected beside `sources`** — move them into the `filesystem()` entry. OpenAPI/AsyncAPI/GraphQL are **not** among these; they're adapters in the top-level `reference` list), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.\n- **`basePath`** (top-level): a site-wide mount point (e.g. `\"/docs\"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).\n- **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the footer's GitHub link when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see \"Config-declared nesting\" above).\n- **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: \"algolia\", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (the assistant, Open in chat — `ai.assistant.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openai({ model })` (or `openai({ baseUrl, name, model, apiKeyEnv })` for any OpenAI-compatible endpoint), `anthropic({ model })`, `gemini({ model })`, `grok({ model })`, `openrouter({ model, reasoning })`, `llmgateway({ model })`, or `inkeep({ model })`. Every adapter takes `model`, `apiKeyEnv`, `headers`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`). Every adapter except `inkeep()` also takes `reasoning`: Inkeep runs its own answer pipeline, so `inkeep({ reasoning })` fails validation with an unrecognized key — drop a source's reasoning setting there and report it. There are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.assistant` — a source that configured an AI assistant that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` sit on `ai.assistant` itself; there is **no** `ai.ask` — Blume 2.0.0 and earlier used that name, so an older `blume.config.ts` moves the whole block to `ai.assistant`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).\n- **`analytics`** — adapters from `blume/analytics`, including `script()` for a source's custom head scripts. A known analytics snippet in the source's head scripts (the gtag loader plus `gtag('config')` pair, GTM, Plausible, …) becomes that provider's factory, not `script()`. Adapters load **only in production builds and wait for `consent`** when it's configured, so a script a page needs to work (a support widget, a form embed) goes in a `PageFooter` layout slot component instead (`defineComponents`).\n- **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: \"static\"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (the assistant, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from \"blume/deploy\"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: \"/docs\" }` (see below for when to add `site`). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from \"blume/deploy\"`) when a config uses both.\n- **Keep the source's site URL as `deployment.site` unless the target host is one Blume auto-detects.** Blume fills `site` in from the platform's build environment only on **Vercel and Netlify** (their stable production domain), and uses the dev server's `localhost` URL during `blume dev`; on those two hosts leave it unset and let detection pick the deployed URL. Everywhere else — Cloudflare Pages (it exposes only the per-deploy `CF_PAGES_URL`), GitHub Pages, S3 or another static host, a custom CDN, Cloudflare Workers, a `node()` server — nothing detects it, and an unset `site` silently drops the sitemap, OG images, RSS feeds, the AI catalog, and absolute canonical URLs. So when the source config had a `url`/`site` field and the target isn't one of those two hosts, carry it over: `deployment: { site: \"https://…\" }` for a static build, or the `site` option of a host adapter (`node({ site })`). If you can't tell where the site will deploy, keep it and say so in the report.\n- **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png` or `apple-touch-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.\n\nThe schema is exported from `blume/schema`; the full field reference is in the `docs/configuration/` directory of the installed `blume` package (see \"Full documentation\" below for how to locate it).\n\n### Icons are Lucide, period\n\nBlume resolves **bare kebab-case [Lucide](https://lucide.dev) names** everywhere an icon is accepted — frontmatter `icon`, `sidebar.icon`, `meta.ts` `icon`, `navigation.tabs`/`selectors` icons, and `Card`/`Step`/`Icon`/etc. props. There is **no** FontAwesome or Tabler support and **no** `iconType` prop. Names must be **kebab-case** (`book-open`, not `BookOpen`) — a PascalCase React-component name (common in Fumadocs/lucide-react sources) does not resolve and renders nothing. When migrating a source that uses another icon set (Mintlify and Fern use FontAwesome), **map each name to its closest Lucide equivalent**; where none exists, drop the icon and report it. Verify a name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it.\n\n### Page frontmatter (strict — unknown keys are build errors)\n\n```yaml\n---\ntitle: Install # renders as the page H1 — remove any duplicate H1 in the body\ndescription: Install Blume and scaffold your first project.\ntype: doc # doc (default); blog and changelog drive feeds, other values only mean something under content.types\nsidebar:\n  label: Install # overrides title in the sidebar\n  order: 2\n  icon: download\n  badge: New\n  hidden: false\nseo:\n  title: …\n  description: …\n  image: /og/install.png\n  canonical: https://…\n  noindex: false\nsearch:\n  exclude: false\n  tags: [api]\nslug: install # override the generated route\ndraft: false\nlastModified: 2026-06-20 # pin the \"last updated\" date\n---\n```\n\nAlso valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog metadata), `deprecated`, and the top-level shorthands `hidden` (for `sidebar.hidden`) and `noindex` (for `seo.noindex`).\n\n### Authoring features (no imports needed in `.mdx`)\n\n- **The rich features are MDX-only.** Directives, `package-install`, mermaid, and math are wired into the MDX processor; in a plain `.md` file a `:::note` stays **literal text** — and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. **Rename any `.md` file that uses (or should use) these to `.mdx` during migration.** MDX is stricter prose than Markdown: any `<` followed by a non-space character that doesn't open a tag (`<7`, `<50 ms`, `<= 5`, `List<T>`) and a bare `{` or `}` fail the build, so escape them (`&lt;` or `\\<`, and `\\{`) in any page you rename. With `lastModified: \"git\"`, every page the migration edits shows the migration commit's date, so say so in the report when the site shows dates; a page it only renames or moves keeps its date (Blume follows renames). This bites hardest on Docusaurus/Starlight sources, whose `.md` content is full of `:::` admonitions. Plain Markdown (headings, tables, fenced code with titles/highlighting) is fine in `.md`.\n- **Callouts as directives:** `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::info`, `:::success`, with an optional title in brackets, which renders its inline Markdown: `:::warning[Heads up]`. Aliases `caution`→warning, `error`→danger, `important`→note, `warn`→warning. A callout that needs its own icon or tint is `<Callout type icon color title>` (a Lucide `icon`, any CSS `color`), which a directive can't set. **markdown-it sources (VitePress, VuePress, Docusaurus v2) write `::: tip Title`: with a space after the colons it's never a directive, even in `.mdx`, and renders as literal text (`.mdx` warns `BLUME_DIRECTIVE_SPACED_NAME` only for a callout name); inside an open `:::` container it closes that container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`); a title after the name (`:::tip Title`) is dropped too (`BLUME_DIRECTIVE_OPENING_TEXT`).** Normalize to `:::tip[Title]`. MDC's two-colon blocks (Docus, any Nuxt Content site: `::note` … `::`) are leaf directives to Blume and print as text (`BLUME_MDC_SYNTAX`), and MDC `[text]{.class}` spans fail the MDX build (`BLUME_MDX_ATTRIBUTE_LIST` names the line). GitHub alerts (`> [!NOTE]`, any of GitHub's five, `[!CAUTION]` as danger) render as callouts in `.mdx`, text after the marker as the title; in `.md` they stay quotes that show the marker (`BLUME_MD_GITHUB_ALERT`), so rename those pages to `.mdx`.\n- **No-import MDX components:** `Callout`, `Card`/`CardGroup`, `Columns`/`Column`, `Steps`/`Step`, `Tabs`/`Tab`, `Accordion`/`AccordionItem`, `Expandable`, `FileTree`, `Tree`/`Tree.Folder`/`Tree.File`, `CodeGroup`, `Frame`, `Panel`, `Tooltip`, `Tile`, `Badge`, `Icon`, `TypeTable`/`AutoTypeTable`, `Color`, `YouTube`, `Visibility`, `GithubInfo`, `Component`, `CodeBlock`, `Diff`, `Prompt`, `Math`, `Changelog`, and the hand-written endpoint fields `ParamField`/`ResponseField`/`RequestExample`/`ResponseExample` (Mintlify's semantics: `<ParamField path|query|header|body=\"name\">` names the field by its location; see `docs/references/api-pages.mdx`). (**Not** shipped — convert away: `<Warning>`/`<Note>`/`<Info>`/`<Tip>`/`<Check>`/`<Error>` → the matching `:::` directive, and `RequestField`. A `<CodeBlock>` with no `code` prop, used as a wrapper around a fence as Fern and Mintlify do, renders the fence as written but ignores its own `title`: unwrap the fence and put the title in its meta. A component that shows its children, used with none and another tool's props (`<Badge type=\"tip\" text=\"beta\" />`), renders empty and warns `BLUME_UNKNOWN_PROP`: put the content between the tags. See the reference files for targets.)\n- **Fenced-code superpowers:** ` ```package-install ` → package-manager tabs; ` ```mermaid ` → a rendered diagram; code-block titles (` ```ts server.ts `), line numbers (`lineNumbers`), and highlighting (`{1,4-5}`, `// [!code ++]`).\n- **Fences and titles that trip migrations:** a fence language Shiki doesn't know renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE` (case doesn't matter: `JSON` highlights like `json`): fix the id, or write `text` for plain text. A language glued to attributes with a comma (` ```rust,ignore `) is one of those; with a space instead, rustdoc's and mdBook's keywords (`ignore`, `no_run`, `should_panic`, `edition2021`, `editable`, …) warn `BLUME_CODE_FENCE_OPTION` and stay out of the title, but any other second word becomes the block's title: keep only the language. Other tools' fence options (`hl_lines=\"2 3\"`, `linenums=\"1\"`, `showLineNumbers`, Mintlify's `lines`, `wordWrap`, `filename=\"…\"`) do nothing, stay out of the title, and warn `BLUME_CODE_FENCE_OPTION` with the Blume spelling (`{2,3}`, `lineNumbers`, `wrap`, `title=\"…\"`). A fence with options but no language needs one (` ```text title=\"x\" lineNumbers `), or the first option is read as the language. A title is every word after the language, and a bracketed one (` ```ts [file.ts] `, from VitePress and MDC) drops its brackets, with a range glued after them (`[file.ts]{2}`) highlighting its lines. `Expandable`, `AccordionItem`, and callout titles render inline Markdown (code, emphasis, links); a `Step` title is a plain string that shows Markdown as written and has no anchor, so keep an anchored heading as the first line inside `<Step>`. Raw HTML `<img src>` and `<a href>` are never optimized: a relative one naming a file beside the page publishes that file with the page (as `[spec](./spec.pdf)` and `<Card img=\"./cover.png\">` do), a root path gains `deployment.base` like a Markdown link, and `blume validate` checks both, but a relative `<a href>` to a page resolves against the page's URL, not its folder. Convert images to Markdown so they're optimized, and page links to Markdown links or root routes.\n- **Math:** `$$…$$` renders in `.mdx` with **no conf\n\n<!-- [TRUNCATED AT 50KB] -->","frontmatter":{"name":"blume-migrate","description":"Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, VitePress, VuePress, Docus, MkDocs, mdBook, Fern, GitBook, ReadMe, Redocly, Docsify, Jekyll (Just the Docs), GitHub Wiki, or any docs framework) to Blume, the markdown-first docs framework on Astro. Translate the source config to blume.config.ts, restructure content into Blume's filesystem-derived navigation, rewrite JSX callouts to directives, convert icons to Lucide, and inline snippets. Use when the user asks to migrate/convert/port a docs repo to Blume, or when the repo has a docs.json/mint.json, docusaurus.config.*, meta.json with fumadocs, _meta.* with nextra, an astro.config.* with starlight(), a .vitepress/config.* or .vuepress/config.*, an mkdocs.yml or zensical.toml, a book.toml (mdBook), a fern/docs.yml, a GitBook .gitbook.yaml/gitbook-docs.yaml, ReadMe _order.yaml files in docs/ or reference/, a Redocly sidebars.yaml or @redocly/realm dependency, an index.html that sets window.$docsify, a docus or @nuxt-themes/docus dependency, a Jekyll _config.yml (Just the Docs theme), or a GitHub wiki clone (Home.md with _Sidebar.md, [[Page Name]] links)."},"isInternal":false,"tokens":12996,"sizeBytes":50331},{"name":"audit-checklist.md","path":".agents/skills/blume-update-docs/references/audit-checklist.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-update-docs/references/audit-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Audit checklist\n\nUse this checklist to decide whether a docs-maintenance run should make changes.\n\n## Sources to check\n\n- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.\n- **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.\n- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.\n- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.\n- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.\n- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.\n- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.\n- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.\n\n## Change criteria\n\nMake a docs edit when at least one condition is true:\n\n- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.\n- A documented workflow no longer works or misses a required step.\n- A page promises a feature, provider, adapter, or integration the code no longer supports.\n- A new user-facing capability shipped but is absent from the appropriate docs page.\n- A link points at moved, removed, or outdated primary documentation.\n- A changelog or release page needs an entry for shipped user-facing behavior.\n\nSkip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.\n\n## Blume editing guidance\n\n- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.\n- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.\n- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.\n- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.\n- Keep internal links root-relative (`/guides/setup`), in the form nearby pages already use.\n- Do not edit generated `.blume/` or `dist/` output.\n\n## PR notes\n\nInclude these sections in the PR body or no-op summary:\n\n- Sources checked\n- Docs changed\n- Verification run (commands and results)\n- Skipped checks, with reasons\n- Remaining risk or follow-up\n","isInternal":false,"tokens":645,"sizeBytes":3016},{"name":"SKILL.md","path":".agents/skills/blume-update-docs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume-update-docs/SKILL.md","title":"blume-update-docs","category":"anthropic-skill","format":"markdown","content":"---\nname: blume-update-docs\ndescription: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.\n---\n\n# Update Blume Docs\n\nBlume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, `blume build` validates frontmatter and duplicate routes, and `blume validate` checks links and anchors.\n\nYour job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.\n\n## Ground rules\n\n- **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.\n- **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.\n- **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.\n- **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.\n- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.\n\n## Workflow\n\n1. **Establish context.**\n   - Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.\n   - Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.\n   - If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.\n\n2. **Reuse or create a maintenance branch.**\n   - If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.\n   - Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.\n\n3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:\n   - Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.\n   - Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.\n   - Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.\n   - Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.\n\n4. **Update the docs.**\n   - Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.\n   - Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.\n\n5. **Verify.**\n   - Run the docs build (`blume build` or the repo's documented docs QA) — it validates frontmatter and duplicate routes.\n   - Run `blume validate` to check internal links and anchors.\n   - Run lint/format/typecheck when the repo's conventions call for them on docs changes.\n   - Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.\n\n6. **Deliver.**\n   - **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.\n   - **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.\n\n## Resources\n\n- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.\n","frontmatter":{"name":"blume-update-docs","description":"Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release."},"isInternal":false,"tokens":1122,"sizeBytes":4958},{"name":"SKILL.md","path":".agents/skills/blume/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/.agents/skills/blume/SKILL.md","title":"blume","category":"anthropic-skill","format":"markdown","content":"---\nname: blume\ndescription: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.\n---\n\n# Blume\n\nBlume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.\n\nThe core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.\n\n## What makes it different\n\n- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.\n- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional in-page **assistant** or an **MCP server** so coding agents read your docs directly.\n- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.\n- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.\n\n## Quickstart\n\nBlume needs **Node.js 22.19 or newer**. From an empty folder:\n\n```bash\nnpx blume init   # scaffold docs/index.mdx, blume.config.ts, and package.json scripts, then install\nnpm run dev      # dev server with hot reload\nnpm run build    # static HTML to dist/, with a local search index\n```\n\nIn a project that already has a `package.json`, `blume init` leaves it alone: add `\"dev\": \"blume dev\"` and `\"build\": \"blume build\"` to its scripts, or run `npx blume dev`. Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.\n\n### Writing a page\n\nEvery page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.\n\n```mdx\n---\ntitle: Introduction\ndescription: Welcome to my docs.\n---\n\nWelcome! Use **Markdown** and built-in components — no imports required:\n\n:::note\nBlume ships callouts, cards, tabs, steps, and more.\n:::\n```\n\nNavigation, search, and page metadata are inferred from your files as you add them.\n\n## Upgrading from Blume 1\n\nBlume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the assistant's model backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), Ask AI is renamed the assistant (`ai.ask` becomes `ai.assistant`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:\n\n```bash\nnpx blume@latest upgrade\n```\n\nIt bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--codex` or `--claude` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.\n\n## Migrating from another framework\n\nTo move a Mintlify, Docusaurus, GitBook, MkDocs, ReadMe, or other docs site to Blume, the user runs `npx blume migrate [source] --codex` (or `--claude`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.\n\nEvery build publishes a generated agent skill for the site at `/skill.md` (`agents.skillMd`). To write a richer one from the docs, the user runs `blume skill --codex` (or `--claude`), which opens an agent on the `blume-write-skill` skill. When you are that agent, or the user asks you to write the site's skill directly, follow `skills/blume-write-skill/SKILL.md` in the installed package.\n\n## What's included\n\n- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.\n- **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).\n- **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.\n- **Narration** — a \"Listen to this page\" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build; `openai({ model, voice })` uses OpenAI's speech models instead, and with a `baseUrl` a self-hosted OpenAI-compatible speech server, and needs `@ai-sdk/openai` installed).\n- **Navigation** — inferred from files, refined with `meta.ts` or config.\n- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.\n- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.\n- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.\n\n## How it works\n\nThe Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.\n\n## Full documentation\n\nThis is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the `docs/` directory bundled inside the installed `blume` package.\n\n**Locate the package first — it is not always at the repository root.** In a workspace monorepo (pnpm especially), `blume` is installed in the depending workspace's `node_modules` (e.g. `apps/docs/node_modules/blume/docs`), not the root. From the package that depends on `blume`, this prints the exact location:\n\n```bash\nnode -e \"console.log(require.resolve('blume/package.json'))\"\n```\n\nThe docs sit in `docs/` next to that `package.json`. Start with `docs/index.mdx` (Introduction) and `docs/01-quickstart.mdx`, then list the `docs/` directory: each section is a folder — configuration, content authoring, API references, discoverability (SEO and the agent-facing surface), the CLI, and advanced topics — so open the one that covers the task.\n","frontmatter":{"name":"blume","description":"Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files."},"isInternal":false,"tokens":1924,"sizeBytes":7931},{"name":"SKILL.md","path":"skills/orpc-contract/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/skills/orpc-contract/SKILL.md","title":"orpc-contract","category":"anthropic-skill","format":"markdown","content":"---\nname: orpc-contract\ndescription: \"Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill.\"\nlicense: MIT\n---\n\n# oRPC Contract-First\n\nContract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with `oc` from `@orpc/contract`, and an implementation built from it with `implement` from `@orpc/server`. Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the `os`-first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with `unlazyRouter` first).\n\nThis skill targets oRPC v2. The `orpc` skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see [Full documentation](#full-documentation)).\n\n## Define the contract with oc\n\nEvery chain is optional and each call returns a new instance, so share base contracts freely. A contract has no `.handler`. Zod, Valibot, ArkType, and any other Standard Schema library work.\n\n```ts\nimport { oc } from '@orpc/contract'\nimport * as z from 'zod'\n\nexport const contract = {\n  planet: {\n    list: oc\n      .output(z.array(z.object({ id: z.number(), name: z.string() }))),\n    find: oc\n      .errors({ NOT_FOUND: {} })\n      .input(z.object({ id: z.number() }))\n      .output(z.object({ id: z.number(), name: z.string() })),\n  },\n}\n```\n\n- Always define `.output`: without it clients infer the output as `unknown`.\n- A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n- Attach shared metadata to a whole subtree with `oc.meta(someMeta).router({...})`.\n- `.errors({ NOT_FOUND: {} })` declares typesafe errors; implementations throw them via `errors.NOT_FOUND()` and clients infer their shapes.\n- Repeated `.input`/`.output` calls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields.\n- Schema-library-free contracts use the `type` utility from `@orpc/contract`: `oc.input(type<{ value: number }>())`, optionally with a mapping function as `type<Input, Output>(fn)`.\n- REST routes attach via `.meta(openapi({ method: 'GET', path: '/planets/{id}' }))` from `@orpc/openapi`, exactly as on `os`; routing rules, `prefix`, and spec generation belong to the `orpc-openapi` skill.\n\nInfer types with `InferRouterContractInputs`, `InferRouterContractOutputs`, and `InferRouterContractErrors` from `@orpc/contract`.\n\n## Implement with implement\n\n`implement` turns the contract into an implementer that mirrors its shape and type-checks every handler; `.router` also enforces the contract at runtime.\n\n```ts\nimport { implement } from '@orpc/server'\n\nconst implementer = implement(contract).$context<{ db: DB }>()\n\nconst listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())\n\nconst findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {\n  const planet = await context.db.find(input.id)\n  if (!planet)\n    throw errors.NOT_FOUND()\n  return planet\n})\n\nexport const router = implementer.router({\n  planet: { list: listPlanets, find: findPlanet },\n})\n```\n\n- `.$context` declares the initial context the procedures require, as on `os`.\n- Apply middleware per procedure with `.use(mw)` before `.handler`. That runs after input validation (the contract already registered `.input`); to wrap validation, apply it at router level: `implementer.use(mw)` for every procedure, or `implementer.planet.use(mw).list` for a subtree. Router-level plus procedure-level `.use` can run the same middleware twice; use the dedupe pattern from the `orpc` skill.\n- `implementer.middleware(fn)` creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the `in` operator: `if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS()`. Any type-compatible middleware also works.\n- The result is a normal router: serve it with `RPCHandler` (`orpc` skill) or `OpenAPIHandler` (`orpc-openapi` skill), call it in-process with `call` or `createRouterClient` from `@orpc/server`.\n\n## Consume the contract from clients\n\n`RPCLink` needs only the contract type; `OpenAPILink` takes the contract as a runtime value to read each procedure's route. Get the client types exactly right:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\n// RPC protocol (server side is RPCHandler)\nconst rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })\nconst client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)\n\n// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)\nconst openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })\nconst apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)\n```\n\n- Router-first equivalents use `RouterClient<typeof router>` from `@orpc/server` in the same positions.\n- `JsonifiedClient` is required over `OpenAPILink` because OpenAPI serialization is one-way (a `Date` returns as a string); dropping it via Smart Coercion, plus `OpenAPILink` options and CORS caveats, are in the `orpc-openapi` skill.\n- Per-call client context is the second type parameter, `RouterContractClient<typeof contract, ClientContext>`, then `client.planet.find(input, { context: { token } })`; link options like `headers` accept functions of that context.\n- Export `RouterContractClient<typeof contract>` as a type from the server package so clients never import the contract module itself (still needed as a runtime value for `OpenAPILink`; ship the minified JSON below).\n- In very large codebases, skip the root client: pin each procedure with `.meta(meta.path([...]))` and build per-procedure clients with `createContractClientFactory` from `@orpc/contract` (`createContractJsonifiedClientFactory` from `@orpc/openapi` when the link needs `JsonifiedClient`); fetch `contract/client-factory` before adopting it.\n\n## Ship the contract\n\nWhen the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:\n\n```ts\nimport fs from 'node:fs'\nimport { minifyRouterContract } from '@orpc/contract'\nimport { unlazyRouter } from '@orpc/server'\n\nconst minified = minifyRouterContract(await unlazyRouter(router))\nfs.writeFileSync('./contract.json', JSON.stringify(minified))\n```\n\n`minifyRouterContract` keeps only client-needed metadata. On the client, import the JSON and cast, since schemas do not survive serialization: `new OpenAPILink(contract as typeof router, ...)`.\n\nTo publish a typed SDK to npm, export a factory that pairs the contract with a link:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nexport function createMyApi(apiKey: string): RouterContractClient<typeof contract> {\n  const link = new RPCLink({\n    origin: 'https://example.com',\n    url: '/rpc',\n    headers: { 'x-api-key': apiKey },\n  })\n  return createORPCClient(link)\n}\n```\n\nBundle with `tsdown --dts src/index.ts`, point `exports` at `dist` types plus import entries, list `@orpc/client` and `@orpc/contract` as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch `recipes/publish-client-to-npm` for the complete `package.json`.\n\n## Generate the contract from an existing OpenAPI spec\n\nUse Hey API's `orpc` plugin instead of hand-writing the contract. Install `@hey-api/openapi-ts@next` as a dev dependency (oRPC v2 output requires the `next` tag until the next stable Hey API release) and create `openapi-ts.config.ts`:\n\n```ts\nimport { defineConfig } from '@hey-api/openapi-ts'\n\nexport default defineConfig({\n  input: 'https://example.com/openapi.json', // local file or URL\n  output: 'src/contract',\n  plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],\n})\n```\n\nThen run `npx @hey-api/openapi-ts`. It writes `orpc.gen.ts` (one procedure contract per operation, routed via `.meta(openapi({...}))` with `inputStructure: 'detailed'`, plus a combined `contract` router) and `zod.gen.ts`. The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install those too. From there, implement the contract on your own server, or point `OpenAPILink` at the existing spec-compliant server.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/contract/procedure.md)\n\nPages to fetch when you need details beyond this skill:\n\n- Contract: `contract/procedure`, `contract/router`, `contract/implementation`, `contract/generate-from-openapi`\n- Clients: `client/client-side`, `client/server-side`, `client/error-handling`, `openapi/link`\n- Workflows: `recipes/publish-client-to-npm`, `contract/client-factory`, `recipes/monorepo-setup`\n","frontmatter":{"name":"orpc-contract","description":"Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill.","license":"MIT"},"isInternal":false,"tokens":2588,"sizeBytes":10741},{"name":"SKILL.md","path":"skills/orpc-migrate/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/skills/orpc-migrate/SKILL.md","title":"orpc-migrate","category":"anthropic-skill","format":"markdown","content":"---\nname: orpc-migrate\ndescription: \"Migrate existing codebases to current oRPC, covering tRPC to oRPC (incremental wrapping via the @orpc/trpc integration or a full rewrite with the concept mapping) and oRPC v1 to v2 (package renames, breaking changes, and a safe order of operations). Use when asked to migrate from tRPC to oRPC, convert or wrap a tRPC router, upgrade oRPC v1 to v2, fix oRPC v2 breaking changes, or swap `@trpc/*` packages for `@orpc/*` equivalents. Biases toward retrieval from the oRPC docs over pre-trained knowledge. Not for greenfield oRPC work or new features in an already-migrated codebase: use the orpc skill for those.\"\nlicense: MIT\n---\n\n# Migrating to oRPC\n\nPlaybook for two migrations: tRPC to oRPC, and oRPC v1 to v2. Work in small mechanical steps and run the project's typecheck and test suite after each one, so any failure points at the last step. Pretrained knowledge of oRPC describes v1 and is often wrong for v2: derive every import path, builder method, and option name from the docs pages listed at the end, never from memory. For core v2 concepts while rewriting (the `os` builder, routers, middleware, clients), load the `orpc` skill. v2 currently ships under the `beta` npm dist-tag and plain installs get v1; drop the `@beta` suffix once `npm view @orpc/server dist-tags` shows `latest` at 2.x.\n\n## tRPC to oRPC\n\nTwo paths. Pick incremental when the app must keep shipping or the tRPC router is large; pick the full rewrite when the router is small enough to convert in one pass.\n\n### Incremental: wrap the existing tRPC router\n\nInstall `@orpc/trpc@beta` and convert. The result is a regular oRPC router: expose it through an RPC or OpenAPI handler, or call it with a server-side client, while the tRPC code keeps working untouched.\n\n```ts\nimport { toORPCRouter } from '@orpc/trpc'\n\nconst orpcRouter = toORPCRouter(trpcRouter)\n```\n\n- tRPC error formatting is not supported: tRPC errors arrive wrapped in `ORPCError` with the `TRPCError` as `cause` (and a `ZodError` below that for validation failures). Reshape them in a handler interceptor if consumers need structured errors.\n- `toTRPCMeta` bridges oRPC `openapi()` metadata into tRPC `.meta()` so converted procedures get OpenAPI routing. Chained tRPC `.meta()` calls merge shallowly, so keep all oRPC metadata inside a single `toTRPCMeta` call.\n\nThen rewrite leaf routers to native oRPC one at a time, mounting each next to the converted router in one plain object.\n\n### Full rewrite: concept map\n\n| Concept           | tRPC                                           | oRPC                           |\n| ----------------- | ---------------------------------------------- | ------------------------------ |\n| Router            | `t.router({...})`                              | plain object                   |\n| Procedure builder | `t.procedure`                                  | `os`                           |\n| Context           | `initTRPC.context<T>()`                        | `os.$context<T>()`             |\n| Create middleware | `t.middleware(fn)`                             | `os.middleware(fn)`            |\n| Use middleware    | `.use(mw)`                                     | `.use(mw)`                     |\n| Validation        | `.input(schema)` / `.output(schema)`           | same names                     |\n| Implementation    | `.query()` / `.mutation()` / `.subscription()` | `.handler()` for all three     |\n| Errors            | `new TRPCError({ code, ... })`                 | `new ORPCError(code, { ... })` |\n| Serializer        | `superjson` transformer                        | built in, remove `superjson`   |\n\nSteps, in order, verifying after each:\n\n1. **Packages.** Remove `@trpc/server`, `@trpc/client`, `@trpc/tanstack-react-query`; install `@orpc/server@beta`, `@orpc/client@beta`, `@orpc/tanstack-query@beta`.\n2. **Base file.** Port the context factory unchanged, then rebuild the shared procedures. In handlers and middleware, `ctx` becomes `context`:\n\n   ```ts\n   import { ORPCError, os } from '@orpc/server'\n\n   const o = os.$context<Awaited<ReturnType<typeof createContext>>>()\n\n   export const publicProcedure = o.use(timingMiddleware)\n   export const protectedProcedure = publicProcedure.use(({ context, next }) => {\n     if (!context.session?.user)\n       throw new ORPCError('UNAUTHORIZED')\n     return next({ context: { session: context.session } })\n   })\n   ```\n\n3. **Procedures.** Replace `.query`/`.mutation`/`.subscription` with `.handler`; `.input` and `.output` carry over as is.\n4. **App router.** Delete every `createTRPCRouter()` wrapper; nested plain objects are the router.\n5. **Server.** Replace the tRPC adapter with an oRPC handler for the runtime (fetch shown; other adapters exist for Node, Fastify, AWS Lambda, WebSocket):\n\n   ```ts\n   import { RPCHandler } from '@orpc/server/fetch'\n\n   const handler = new RPCHandler(appRouter)\n\n   const { response } = await handler.handle(request, {\n     prefix: '/api/orpc',\n     context: await createContext({ headers: request.headers }),\n   })\n   ```\n\n6. **Client.** `RPCLink` plus `createORPCClient`, typed by `RouterClient`. Call sites drop the `.query()`/`.mutate()` suffixes:\n\n   ```ts\n   import type { RouterClient } from '@orpc/server'\n   import { createORPCClient } from '@orpc/client'\n   import { RPCLink } from '@orpc/client/fetch'\n\n   const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })\n   export const client: RouterClient<typeof appRouter> = createORPCClient(link)\n\n   const { planets } = await client.planet.list({ cursor: 0 })\n   ```\n\n7. **TanStack Query.** `createTanstackQueryUtils(client)` replaces the provider and `useTRPC` hook entirely; use the utils object directly. Input moves inside an `input` key: `orpc.planet.list.queryOptions({ input: { cursor: 0 } })`, `orpc.planet.create.mutationOptions()`. For infinite queries, `infiniteOptions` takes `input` as a function of the page param.\n\n## oRPC v1 to v2\n\nMost v1 names still compile through deprecated aliases (strike-through hints, not errors), so migrate in passes. Order of operations:\n\n1. **Update packages.** Install every `@orpc/*` package from the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`, and so on). Swap renamed ones first: `@orpc/react-query`/`@orpc/vue-query`/`@orpc/solid-query`/`@orpc/svelte-query` all became `@orpc/tanstack-query`; `@orpc/openapi-client` merged into `@orpc/openapi`; `@orpc/react` became `@orpc/next`; `@orpc/otel` became `@orpc/opentelemetry`; the `experimental-` packages were promoted (`@orpc/publisher`, `@orpc/ratelimit`, `@orpc/pino`, `@orpc/swr`); `@orpc/vue-colada` became `@orpc/pinia-colada`. Typecheck: the remaining errors are the hard breaks.\n2. **Fix the hard breaks** (no aliases):\n   - Routing: `.route`, `.prefix`, `.tag`, `.$route` are gone from the builder. Use `.meta(openapi({ method, path, prefix, tags }))` from `@orpc/openapi`, or restore `.route` with `import '@orpc/openapi/extensions/route'`.\n   - `.callable` and `.actionable`: use `call`/`createRouterClient` from `@orpc/server` and `createServerFunctionable` from `@orpc/next`, or the corresponding extension imports.\n   - `RPCLink`: the single `url` split into `origin` plus a path-only `url`.\n   - Errors: `status` was removed from `ORPCError` and `.errors` definitions; map codes to HTTP status with `errorStatusMap` on the handler.\n   - `safe()`: the third tuple element is now the typed error itself (or `null`) and a fourth `isSuccess` element was added.\n   - Option renames, scoped: handler `rootInterceptors` to `routingInterceptors` (handler `clientInterceptors` still exists, unchanged); link `clientInterceptors` to `transportInterceptors`. Flat `eventIterator*` options moved under the adapter's request/response mapping: `toFetchResponse.eventStream` on the fetch handler, `sendStandardResponse.eventStream` on Node, `toFetchRequest.eventStream` on the link.\n   - `adapterInterceptors` was removed from handlers and links, because regular interceptors can now customize body parsing behavior.\n3. **Audit silent behavior changes** (compile fine, behave differently):\n   - **Wire format changed:** a v1 link cannot talk to a v2 server, in either direction. Deploy the upgraded server and clients together.\n   - **Automatic middleware deduplication removed:** middleware applied at both router and procedure level now runs twice, with no warning. Guard shared middleware with the context-flag pattern from the dedupe-middleware recipe.\n   - **Batch Plugin `exclude` became `filter` with the opposite meaning.** Usually delete `exclude`; if skipping is still needed, negate the predicate.\n   - **`RPCHandler` rejects GET by default** (`allowMethods` defaults to POST/PUT/PATCH/DELETE). Simplest fix: stop sending GET from the link; only allow GET deliberately, with CSRF protection.\n   - **Handler `filter` takes positional arguments now;** the v1 destructured form still type-checks but reads wrong values.\n   - **`.input`/`.output` now stack:** a repeated call adds a schema instead of replacing the previous one.\n4. **Sweep deprecated aliases** last: `eventIterator` to `asyncIteratorObject`, handler plugins gained a `HandlerPlugin` suffix and link plugins a `LinkPlugin` suffix, `ContractRouter*` types became `RouterContract*`. The from-v1 guide ends with the full alias cheat sheet.\n\nVerification: typecheck and unit tests after steps 1, 2, and 4; step 3 needs integration or e2e tests, since those changes never surface at compile time. Before finishing, grep for old package names and remaining deprecation strike-throughs.\n\n## Docs retrieval\n\nFetch pages instead of recalling them, and if this skill and a fetched page disagree, trust the page. The v2 docs live at https://orpc.dev and the v1 docs at https://v1.orpc.dev; slugs look alike across both hosts, so check which host a page came from before copying anything from it. The index of every v2 docs page is at https://orpc.dev/llms.txt, https://orpc.dev/llms-full.txt bundles the entire docs in one large file, and appending `.md` to any page URL returns its exact source markdown.\n\nAuthoritative pages to consult during the migration (this skill deliberately omits their full mapping tables):\n\n- https://orpc.dev/docs/migrations/from-trpc : side-by-side tRPC/oRPC code for every step, including server setup per framework\n- https://orpc.dev/docs/migrations/from-v1 : every v2 breaking change with v1/v2 comparisons, package rename table, and the deprecated alias cheat sheet\n- https://orpc.dev/docs/integrations/trpc : `toORPCRouter` and `toTRPCMeta` reference for the incremental path\n","frontmatter":{"name":"orpc-migrate","description":"Migrate existing codebases to current oRPC, covering tRPC to oRPC (incremental wrapping via the @orpc/trpc integration or a full rewrite with the concept mapping) and oRPC v1 to v2 (package renames, breaking changes, and a safe order of operations). Use when asked to migrate from tRPC to oRPC, convert or wrap a tRPC router, upgrade oRPC v1 to v2, fix oRPC v2 breaking changes, or swap `@trpc/*` packages for `@orpc/*` equivalents. Biases toward retrieval from the oRPC docs over pre-trained knowledge. Not for greenfield oRPC work or new features in an already-migrated codebase: use the orpc skill for those.","license":"MIT"},"isInternal":false,"tokens":2595,"sizeBytes":10608},{"name":"SKILL.md","path":"skills/orpc-openapi/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/skills/orpc-openapi/SKILL.md","title":"orpc-openapi","category":"anthropic-skill","format":"markdown","content":"---\nname: orpc-openapi\ndescription: \"Expose an oRPC router as a spec-compliant OpenAPI HTTP API. Use when a project depends on @orpc/openapi, or for defining REST-style routes on oRPC procedures (openapi() metadata or .route with method, path, successStatus), serving them with OpenAPIHandler alongside RPCHandler, coercing query and path strings with Smart Coercion, calling an OpenAPI-shaped API with OpenAPILink, generating an OpenAPI 3.2 (or 3.1, 3.0) document with OpenAPIGenerator, or serving Scalar or Swagger docs with the OpenAPI Reference plugin. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For contract-first work (defining contracts with oc, implementing them with implement, or generating a contract from an existing OpenAPI spec), use the orpc-contract skill; for plain RPC serving, core builder, middleware, or client work with no REST exposure, use the orpc skill instead.\"\nlicense: MIT\n---\n\n# oRPC over OpenAPI\n\noRPC procedures speak two protocols from one router: the RPC protocol (`RPCHandler`/`RPCLink`) and plain OpenAPI HTTP (`OpenAPIHandler`/`OpenAPILink`). This skill covers the OpenAPI side. For core builder, middleware, context, and client concepts, load the `orpc` skill (it also carries the v2 install and version check); for contract-first workflows with `@orpc/contract`, load the `orpc-contract` skill.\n\nThis skill targets oRPC v2. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its exact docs page first (see [Full documentation](#full-documentation)).\n\n## Routing\n\nA procedure defaults to `POST` with a path derived from the router structure (`planet.create` becomes `POST /planet/create`). Override with `openapi` metadata, on server (`os`) and contract (`oc`) builders alike:\n\n```ts\nimport { openapi } from '@orpc/openapi'\nimport { os } from '@orpc/server'\nimport { z } from 'zod'\n\nconst getPlanet = os\n  .meta(openapi({ method: 'GET', path: '/planets/{id}', successStatus: 200 }))\n  .input(z.object({ id: z.string() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n- Path params: put `{id}` in `path` and the same key as a required field in the input schema. Use `{+path}` for catch-all segments that may contain `/`.\n- `prefix` prepends a path to a procedure or a whole router: `os.meta(openapi({ prefix: '/api/v2' })).router({...})`. Always set a `prefix` on lazy routers so lazy loading only triggers for relevant requests.\n- Merging across repeated `.meta(openapi(...))` calls: `prefix` and `tags` concatenate, `method`/`path`/`successStatus` last-wins, and setting a field to `undefined` resets it to the default.\n- `successStatus` defaults to `200` and must be below 400.\n\nFor direct routing without `.meta(openapi(...))`, enable the `.route` extension with a side-effect import in a module that always runs at startup (base builder file or server entry):\n\n```ts\nimport '@orpc/openapi/extensions/route'\n\nconst ping = os.route({ method: 'GET', path: '/ping' }).handler(async () => 'pong')\n```\n\n## Input and output mapping\n\nCompact mode (default): path params merge with query params or the request body depending on the HTTP method, so `GET /planets/earth?q=life` yields input `{ id: 'earth', q: 'life' }`. The handler's return value becomes the response body with status `successStatus`.\n\nSet `inputStructure: 'detailed'` in the metadata to receive `{ params, query, headers, body }` instead (define only the fields you need in the input schema). Set `outputStructure: 'detailed'` to return `{ status?, headers?, body? }` and vary the status per response.\n\nQuery strings and form data are decoded with bracket notation (fetch `openapi/bracket-notation` before designing schemas for nested query input; its limits are not guessable):\n\n- Repeated keys become arrays: `?color=red&color=blue` gives `['red', 'blue']`; `color[]=red` pushes too.\n- `[number]` targets an array index; `[key]` targets an object property: `?filter[status]=active` gives `{ filter: { status: 'active' } }`.\n- It cannot represent empty objects or arrays, root-level arrays, or objects whose keys are all numbers, and query/form values always arrive as strings (or files, in form data).\n\nOverride decoding per parameter with `paramsStyles` (`'primitive'`, `'comma-delimited-array'`, `'comma-delimited-object'`) and `queryStyles` (those plus `'array'`, `'json'`, and space/pipe-delimited variants). Use `requestBodyHint`/`responseBodyHint` (`'json'`, `'form-data'`, `'event-stream'`, `'octet-stream'`, `'file'`, ...) when headers alone cannot tell the parser how to handle the body, for example a raw `ReadableStream` upload.\n\n## Serving: OpenAPIHandler\n\nImport from the adapter subpath (`@orpc/openapi/fetch`, `@orpc/openapi/node`, ...):\n\n```ts\nimport { SmartCoercionHandlerPlugin } from '@orpc/json-schema'\nimport { OpenAPIHandler } from '@orpc/openapi/fetch'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new SmartCoercionHandlerPlugin({ converters: [new ZodToJsonSchemaConverter()] }),\n  ],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} })\n  return matched ? response : new Response('Not Found', { status: 404 })\n}\n```\n\n`OpenAPIHandler` coexists with `RPCHandler`: both accept the same router, so mount them on different prefixes (for example `/api` and `/rpc`) and try each in turn, returning the first `matched` response.\n\nSmart Coercion: query, path, and form values arrive as strings, so add `SmartCoercionHandlerPlugin` whenever input schemas expect non-string types from those sources. It coerces schema-driven, lossless conversions only (`'123'` to `123`, `'true'`/`'on'` to `true`, ISO strings to `Date`, arrays to `Set`/`Map` via `x-native-type`) and leaves ambiguous values untouched. Skip it when you already coerce in the schema or performance is critical; it adds runtime overhead.\n\nOther handler options: `interceptors`/`routingInterceptors`/`clientInterceptors` for logging and error mapping, `filter` to exclude procedures from matching, and `errorStatusMap` plus `customErrorResponseBodyEncoder` to customize error responses (by default `ORPCError` codes map to statuses via `COMMON_ERROR_STATUS_MAP`, for example `NOT_FOUND` to 404).\n\n## Calling: OpenAPILink\n\n`OpenAPILink` calls an OpenAPI-shaped oRPC API (or any spec-compliant server) through a typesafe client. It needs the contract or router type to know each procedure's route:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\nconst link = new OpenAPILink(contract, {\n  origin: 'https://api.example.com',\n  url: '/api',\n  headers: ({ context }) => ({\n    authorization: context?.token ? `Bearer ${context.token}` : undefined,\n  }),\n})\n\nconst client: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)\n```\n\nWith a router instead of a contract, type the client as `JsonifiedClient<RouterClient<typeof router>>` (`RouterClient` from `@orpc/server`). `JsonifiedClient` exists because OpenAPI serialization is one-way: a `Date` returns as a string. Add `SmartCoercionLinkPlugin` from `@orpc/json-schema` (same converters, first argument is the contract) to restore native types on responses, then drop the `JsonifiedClient` wrapper from the client type.\n\nTo ship a contract to clients without bundling server code, minify it to JSON and import it with a cast; the flow is in the `orpc-contract` skill under \"Ship the contract\".\n\n## Spec document and interactive docs\n\n`OpenAPIGenerator` turns a router or contract into an OpenAPI 3.2 document (pass any `3.1.x` or `3.0.x` `version` for tools that stop at an older version; `QUERY` procedures need 3.2). `OpenAPIReferenceHandlerPlugin` serves the spec at `/spec.json` and a Scalar UI at `/` under the handler prefix (change with `specPath`/`docsPath`, or set `provider: 'swagger'` for Swagger UI):\n\n```ts\nimport { OpenAPIGenerator } from '@orpc/openapi'\nimport { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] })\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new OpenAPIReferenceHandlerPlugin({\n      spec: () => generator.generate(router, {\n        base: {\n          info: { title: 'Planet API', version: '1.0.0' },\n          servers: [{ url: '/api' }], // absolute URL in production\n        },\n      }),\n    }),\n  ],\n})\n```\n\nEnrich the document through `openapi` metadata: `operationId`, `summary`, `description`, `tags`, `successDescription`, and a `spec` callback that receives the generated operation object and returns an extended one (security requirements, extra responses). Write `spec` and `base` as OpenAPI 3.2 objects even when generating 3.1 or 3.0; the generator downgrades the whole document. Converters also exist for Valibot (`@orpc/valibot`) and ArkType (`@orpc/arktype`); schemas without a matching converter fall back to Standard JSON Schema conversion.\n\nVerify the wiring before declaring success: request `/spec.json` under the handler prefix and one routed endpoint, and confirm the method, path, and status you configured.\n\n## Contract-first\n\nContract-first workflows belong to the `orpc-contract` skill: defining the shape with `oc` from `@orpc/contract`, implementing it with `implement` from `@orpc/server`, consuming the contract from clients, shipping it as minified JSON, and generating it from an existing OpenAPI spec with Hey API. On the OpenAPI side a contract behaves exactly like a router: attach routes with `.meta(openapi({...}))` on `oc` as shown in [Routing](#routing), serve the implemented router with `OpenAPIHandler` as usual, generate the spec document from the contract directly, and give `OpenAPILink` the contract as its runtime value.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/openapi/routing.md)\n\nPages to fetch when you need details beyond this skill:\n\n- OpenAPI: `openapi/routing`, `openapi/input-and-output-mapping`, `openapi/bracket-notation`, `openapi/serializer`, `openapi/handler`, `openapi/link`, `openapi/specification`, `openapi/scalar`\n- Plugins: `plugins/smart-coercion`, `plugins/openapi-reference`\n","frontmatter":{"name":"orpc-openapi","description":"Expose an oRPC router as a spec-compliant OpenAPI HTTP API. Use when a project depends on @orpc/openapi, or for defining REST-style routes on oRPC procedures (openapi() metadata or .route with method, path, successStatus), serving them with OpenAPIHandler alongside RPCHandler, coercing query and path strings with Smart Coercion, calling an OpenAPI-shaped API with OpenAPILink, generating an OpenAPI 3.2 (or 3.1, 3.0) document with OpenAPIGenerator, or serving Scalar or Swagger docs with the OpenAPI Reference plugin. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For contract-first work (defining contracts with oc, implementing them with implement, or generating a contract from an existing OpenAPI spec), use the orpc-contract skill; for plain RPC serving, core builder, middleware, or client work with no REST exposure, use the orpc skill instead.","license":"MIT"},"isInternal":false,"tokens":2698,"sizeBytes":10904},{"name":"SKILL.md","path":"skills/orpc/SKILL.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/skills/orpc/SKILL.md","title":"orpc","category":"anthropic-skill","format":"markdown","content":"---\nname: orpc\ndescription: \"Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.\"\nlicense: MIT\n---\n\n# oRPC\n\noRPC is a typesafe API framework: write plain TypeScript functions on the server, call them from clients like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step. The same router can also be served as a REST API with an OpenAPI spec.\n\nThis skill targets oRPC v2. Check what is installed before writing code: `npm ls @orpc/server` (or any `@orpc/*` package). A 1.x version means v1, where this skill's guidance does not apply; use the `orpc-migrate` skill to upgrade. v2 currently ships under the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`; a plain install silently gets v1). If `npm view @orpc/server dist-tags` shows `latest` at 2.x, the beta has ended: install normally.\n\nPretrained oRPC knowledge describes v1 and is often wrong for v2 (routing moved to `.meta(openapi(...))`, `RPCLink` split `url` into `origin` plus a path, automatic middleware dedupe was removed). Prefer retrieval: the index of every docs page is at https://orpc.dev/llms.txt; see [Full documentation](#full-documentation) for the mechanics.\n\nPackage map:\n\n- `@orpc/server`: the `os` builder, routers, middleware, `RPCHandler`, server-side clients (`call`, `createRouterClient`), `implement` for mocks\n- `@orpc/client`: `createORPCClient`, `RPCLink`, `safe`, `createSafeClient`, `isDefinedError`\n- `@orpc/contract`: contract-first API definitions implemented separately from their logic (see the `orpc-contract` skill)\n- `@orpc/openapi`: `OpenAPIHandler`, `OpenAPILink`, OpenAPI 3.2 spec generation\n- Integration packages such as `@orpc/tanstack-query` and `@orpc/nest`\n\n## Define procedures\n\nBuild a procedure with the `os` builder: describe input with a schema, implement with `.handler`. Zod, Valibot, ArkType, and any other [Standard Schema](https://standardschema.dev/) library work for `.input`, `.output`, and error `data`.\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const listPlanets = os\n  .handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments\n\nexport const findPlanet = os\n  .input(z.object({ id: z.number() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n`.handler` is the only required step. The full chain, every other step optional:\n\n```ts\nconst example = os\n  .$context<{ headers: Headers }>() // initial context this procedure requires\n  .errors({ NOT_FOUND: {} }) // typed errors\n  .use(requireAuth) // middleware\n  .input(z.object({ id: z.number() }))\n  .output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking\n  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))\n```\n\nEvery builder step returns a new instance, so share base builders freely: `const authed = os.use(requireAuth)` then build many procedures from `authed`.\n\n## Assemble a router\n\nA router is a plain object mapping keys to procedures (or nested routers). Do not use the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n\n```ts\nexport const router = {\n  planet: { list: listPlanets, find: findPlanet },\n  admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree\n  planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router\n}\n```\n\nInfer types with `InferRouterInputs` / `InferRouterOutputs` from `@orpc/server`. Applying `.use` at both router and procedure level can run the same middleware twice; see the dedupe pattern below.\n\n## Middleware and context\n\nContext comes from two places. Initial context is declared with `.$context` and passed explicitly when serving or calling (environment values: headers, env, db). Injected context is added at runtime by middleware via `next({ context })` (runtime values: the authenticated user).\n\n```ts\nimport { ORPCError, os } from '@orpc/server'\n\nconst base = os.$context<{ headers: Headers }>()\n\nconst requireAuth = base.middleware(async ({ context, next }) => {\n  const user = await parseUser(context.headers)\n  if (!user) {\n    throw new ORPCError('UNAUTHORIZED')\n  }\n  return next({ context: { user } }) // handler now sees context.user, typed non-null\n})\n```\n\n`.use` accepts named middleware or inline functions. Middleware registered before `.input` runs before validation, the rest after. Middleware can also declare typed input (`os.middleware(async ({ next }, id: number) => ...)`); adapt mismatched shapes with `.use(mw.adaptInput(input => input.id))`.\n\nBest practice, dedupe expensive middleware: the same middleware can run twice in one call (router-level plus procedure-level `.use`, or a procedure `call`ing another). Cache the result in context:\n\n```ts\nconst authProvider = os\n  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()\n  .middleware(async ({ context, next }) => {\n    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)\n    return next({ context: { auth, authLoaded: true } })\n  })\n```\n\n## Typesafe errors\n\nThrow `ORPCError` (a `code` plus optional `message` and `data`). Both `message` and `data` are sent to the client, so never put secrets in them. Throw only `Error` instances, never literals.\n\nDefine errors with `.errors` so clients can infer each error's shape:\n\n```ts\nconst find = os\n  .errors({\n    NOT_FOUND: { message: 'Planet not found' }, // default message\n    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },\n  })\n  .handler(async ({ input, errors }) => {\n    throw errors.NOT_FOUND()\n  })\n```\n\n`throw new ORPCError('NOT_FOUND')` inside that handler is converted to the matching typed error when code and data match. Convert custom error classes to `ORPCError` in a middleware `try/catch`.\n\n## Serve with RPCHandler\n\n`RPCHandler` matches requests to procedures, validates input, runs handlers, and encodes results. Pick the adapter for your runtime. Fetch API (Bun, Deno, Cloudflare Workers):\n\n```ts\nimport { onError } from '@orpc/server'\nimport { RPCHandler } from '@orpc/server/fetch'\nimport { CORSHandlerPlugin } from '@orpc/server/plugins'\n\nconst handler = new RPCHandler(router, {\n  plugins: [new CORSHandlerPlugin()],\n  interceptors: [onError(error => console.error(error))],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, {\n    prefix: '/rpc',\n    context: { headers: request.headers }, // provide the router's initial context here\n  })\n  return matched ? response : new Response('Not found', { status: 404 })\n}\n// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workers\n```\n\nNode HTTP:\n\n```ts\nimport { createServer } from 'node:http'\nimport { RPCHandler } from '@orpc/server/node'\n\nconst handler = new RPCHandler(router)\n\nconst server = createServer(async (req, res) => {\n  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })\n  if (matched)\n    return\n  res.statusCode = 404\n  res.end('Not found')\n})\n\nserver.listen(3000)\n```\n\nUnmatched requests fall through to your own handling. By default `RPCHandler` accepts only `POST`, `PUT`, `PATCH`, and `DELETE`; enabling `GET` via `allowMethods` is a CSRF risk with cookie auth, see [RPC Handler](https://orpc.dev/docs/rpc/handler). Handler options also include `interceptors`, `routingInterceptors`, `clientInterceptors`, `plugins`, `filter`, and `errorStatusMap`. Adapters also exist for [AWS Lambda](https://orpc.dev/docs/adapters/aws-lambda), [Fastify](https://orpc.dev/docs/adapters/fastify), [WebSocket](https://orpc.dev/docs/adapters/websocket), [Message Port](https://orpc.dev/docs/adapters/message-port), and [Expo](https://orpc.dev/docs/adapters/expo).\n\n## Call procedures\n\nServer side (same process, no HTTP; also the fastest way to test procedures):\n\n```ts\nimport { call, createRouterClient } from '@orpc/server'\n\nconst planet = await call(findPlanet, { id: 1 }, { context: { headers } })\n\nconst client = createRouterClient(router, { context: { headers } }) // context can be a function\nconst planets = await client.planet.list()\n```\n\nClient side, `RPCLink` turns calls into HTTP requests. Import the router as a type only so no server code reaches the client bundle:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from '../server/router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({\n  origin: 'http://127.0.0.1:3000',\n  url: '/rpc', // must match the server's prefix\n  headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions\n})\n\nexport const orpc: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await orpc.planet.find({ id: 1 })\n```\n\nClient error handling: plain `try/catch` works, but `safe` preserves typed error inference:\n\n```ts\nimport { createSafeClient, isDefinedError, safe } from '@orpc/client'\n\nconst [error, data] = await safe(orpc.planet.find({ id: 1 }))\nif (isDefinedError(error)) {\n  console.log(error.code, error.data) // typed from the procedure's .errors\n}\nelse if (error) {\n  // unknown error\n}\n\nconst safeClient = createSafeClient(orpc) // every call returns [error, data]\n```\n\n## Beyond the basics\n\n- Streaming / SSE: return an async generator from `.handler`, validate events with `asyncIteratorObject`, resume via `lastEventId`: [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object)\n- OpenAPI: serve the same router as REST with routing metadata plus `OpenAPIHandler`, and generate a spec (covered in depth by the `orpc-openapi` skill): [OpenAPI Handler](https://orpc.dev/docs/openapi/handler)\n- Contract-first: define contracts with `@orpc/contract`, implement with `implement` (covered in depth by the `orpc-contract` skill): [Contracts](https://orpc.dev/docs/contract/procedure)\n- Plugins for handler and link: batch, CORS, dedupe, retry, compression, request limits, smart coercion, static files, timeout, tmp file upload, and more. Fetch a plugin's docs page before configuring it; option names are not guessable: [Plugins](https://orpc.dev/docs/plugins/batch)\n- Integrations: [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) (`createTanstackQueryUtils`), [SWR](https://orpc.dev/docs/integrations/swr), [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada), [Next.js](https://orpc.dev/docs/integrations/next), [NestJS](https://orpc.dev/docs/integrations/nest), [AI SDK](https://orpc.dev/docs/integrations/ai-sdk), [MCP](https://orpc.dev/docs/integrations/mcp) (`McpRegistrar` exposes procedures as MCP tools), [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry)\n- Testing: `call` procedures directly; mock with `implement(router.planet.list).handler(() => [])`, and run the project's typecheck before declaring success, since end-to-end types are oRPC's first correctness signal: [Testing and Mocking](https://orpc.dev/docs/recipes/testing-and-mocking)\n- Monorepos: TypeScript project references keep client types resolvable: [Monorepo Setup](https://orpc.dev/docs/recipes/monorepo-setup)\n- Migrating from tRPC or oRPC v1: use the `orpc-migrate` skill\n\n## Full documentation\n\nThis skill is an overview; fetch exact docs instead of guessing APIs, and if this skill and a fetched page disagree, trust the page. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/procedure.md)\n\nDoc map, all under `https://orpc.dev/docs/`:\n\n- Top level: `procedure`, `router`, `middleware`, `context`, `error-handling`, `metadata`, plus `binary-data` (file uploads) and `async-iterator-object` (streaming/SSE)\n- `rpc/*`, `openapi/*`: protocol details, handlers, links; `contract/*`: contract-first (the `orpc-contract` skill)\n- `client/*`: server- and client-side clients, error handling, `DynamicLink`\n- `adapters/*`: per-runtime serving quirks (fetch-api, node-http, aws-lambda, fastify, websocket, message-port, expo)\n- `plugins/*`: twenty handler/link plugins; `helpers/*`: cookie, encryption, form-data, lock, publisher, ratelimit, signing, base64url\n- `integrations/*`: framework glue; `recipes/*`: guidance (testing, SSR, monorepos, validation)\n- `migrations/from-v1`, `migrations/from-trpc`: upgrades (use the `orpc-migrate` skill)\n","frontmatter":{"name":"orpc","description":"Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.","license":"MIT"},"isInternal":false,"tokens":3408,"sizeBytes":13440},{"name":"README.md","path":"packages/ai-sdk/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/ai-sdk/README.md","title":"ai-sdk Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ai-sdk\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fai-sdk?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7924,"sizeBytes":31355},{"name":"README.md","path":"packages/arktype/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/arktype/README.md","title":"arktype Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/arktype\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Farktype?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7924,"sizeBytes":31357},{"name":"README.md","path":"packages/bun/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/bun/README.md","title":"bun Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/bun\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fbun?logo=npm\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7868,"sizeBytes":31173},{"name":"README.md","path":"packages/client/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/client/README.md","title":"client Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/client\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fclient?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7921,"sizeBytes":31355},{"name":"README.md","path":"packages/cloudflare/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/cloudflare/README.md","title":"cloudflare Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/cloudflare\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcloudflare?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7923,"sizeBytes":31363},{"name":"README.md","path":"packages/contract/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/contract/README.md","title":"contract Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/contract\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fcontract?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7922,"sizeBytes":31359},{"name":"README.md","path":"packages/effect/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/effect/README.md","title":"effect Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-effect\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-effect?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7924,"sizeBytes":31381},{"name":"README.md","path":"packages/evlog/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/evlog/README.md","title":"evlog Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/evlog\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fevlog?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7924,"sizeBytes":31353},{"name":"README.md","path":"packages/hibernation/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/hibernation/README.md","title":"hibernation Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/hibernation\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fhibernation?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7926,"sizeBytes":31365},{"name":"README.md","path":"packages/json-schema/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/json-schema/README.md","title":"json-schema Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/json-schema\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fjson-schema?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7923,"sizeBytes":31365},{"name":"README.md","path":"packages/lock/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/lock/README.md","title":"lock Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/experimental-lock\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fexperimental-lock?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7924,"sizeBytes":31377},{"name":"README.md","path":"packages/mcp/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/mcp/README.md","title":"mcp Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/mcp\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmcp?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7811,"sizeBytes":30953},{"name":"README.md","path":"packages/msw/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/msw/README.md","title":"msw Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/msw\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fmsw?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7923,"sizeBytes":31349},{"name":"README.md","path":"packages/nest/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/nest/README.md","title":"nest Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/nest\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnest?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7922,"sizeBytes":31351},{"name":"README.md","path":"packages/next/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/next/README.md","title":"next Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/next\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnext?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7922,"sizeBytes":31351},{"name":"README.md","path":"packages/node/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/node/README.md","title":"node Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/node\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fnode?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7921,"sizeBytes":31351},{"name":"README.md","path":"packages/openapi/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/openapi/README.md","title":"openapi Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/openapi\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopenapi?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7923,"sizeBytes":31357},{"name":"README.md","path":"packages/opentelemetry/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/opentelemetry/README.md","title":"opentelemetry Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/opentelemetry\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fopentelemetry?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7925,"sizeBytes":31369},{"name":"README.md","path":"packages/pinia-colada/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/pinia-colada/README.md","title":"pinia-colada Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pinia-colada\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpinia-colada?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7927,"sizeBytes":31367},{"name":"README.md","path":"packages/pino/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/pino/README.md","title":"pino Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/pino\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpino?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7923,"sizeBytes":31351},{"name":"README.md","path":"packages/publisher/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/publisher/README.md","title":"publisher Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/publisher\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fpublisher?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7922,"sizeBytes":31361},{"name":"README.md","path":"packages/ratelimit/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/ratelimit/README.md","title":"ratelimit Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ratelimit\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fratelimit?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7925,"sizeBytes":31361},{"name":"README.md","path":"packages/server/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/server/README.md","title":"server Documentation","category":"plugin-manifest","format":"markdown","content":"<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/server\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fserver?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n","isInternal":false,"tokens":7921,"sizeBytes":31355},{"name":"README.md","path":"packages/shared/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/shared/README.md","title":"shared Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fshared%2FREADME.md -->","isInternal":false,"tokens":7921,"sizeBytes":31355},{"name":"README.md","path":"packages/swr/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/swr/README.md","title":"swr Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fswr%2FREADME.md -->","isInternal":false,"tokens":7923,"sizeBytes":31349},{"name":"README.md","path":"packages/tanstack-query/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/tanstack-query/README.md","title":"tanstack-query Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftanstack-query%2FREADME.md -->","isInternal":false,"tokens":7926,"sizeBytes":31371},{"name":"README.md","path":"packages/trpc/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/trpc/README.md","title":"trpc Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Ftrpc%2FREADME.md -->","isInternal":false,"tokens":7923,"sizeBytes":31351},{"name":"README.md","path":"packages/valibot/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/valibot/README.md","title":"valibot Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fvalibot%2FREADME.md -->","isInternal":false,"tokens":7926,"sizeBytes":31357},{"name":"README.md","path":"packages/zod/README.md","rawUrl":"https://raw.githubusercontent.com/middleapi/orpc/HEAD/packages/zod/README.md","title":"zod Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/middleapi/orpc?path=packages%2Fzod%2FREADME.md -->","isInternal":false,"tokens":7923,"sizeBytes":31349}],"systemPromptSnippet":"<yakaai_skills repo=\"middleapi/orpc\">\n<!-- File: CLAUDE.md (Tokens: ~690 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository\n\noRPC — typesafe API framework. pnpm monorepo (Node 22+, `pnpm` workspaces) with packages in `packages/*`, docs/website in `apps/content` (Blume), example apps in `playgrounds/*`, root-level e2e tests in `tests/`, and benchmarks in `benches/`.\n\n## Commands\n\n```bash\npnpm install                 # setup\npnpm test                    # all tests: per-package tests + root vitest\npnpm vitest run <path>       # single test file (from repo root), e.g. pnpm vitest run packages/server/src/builder.test.ts\npnpm type:check              # tsc across all packages — this is also what runs the *.test-d.ts type tests\npnpm lint                    # eslint (also the formatter — @antfu/eslint-config)\npnpm lint:fix\npnpm bench                   # vitest bench\npnpm --filter @orpc/server prepack # build one package (unbuild, runs automatically on pack/publish); builds are NOT needed for dev/tests\n```\n\n- Root vitest config (`vitest.config.ts`) runs all `*.test.ts` with `globals: true`, plus a jsdom project for `*.test.tsx` in `packages/next`, `packages/tanstack-query`, `packages/pinia-colada` and `packages/swr`.\n- Exceptions: `packages/bun` runs with `bun test`, `packages/cloudflare` with its own vitest (workerd) — both are excluded from the root vitest and root tsconfig. Run their tests via `pnpm --filter <pkg> test`. `packages/nest` is excluded only from the root tsconfig (type-check it with `pnpm --filter @orpc/nest type:check`); its tests run in the root vitest.\n- Docs site (Blume): `cd apps/content && pnpm dev` (`blume dev`); `pnpm build` runs `blume build`. Content lives in `.mdx` files; custom pages in `apps/content/pages/`. The dev server caches content-collection frontmatter for custom pages — restart it after editing blog-post frontmatter. `pnpm docs:validate` (root) runs the JSDoc backlink checker plus `blume validate --strict` (links, anchors, assets); CI runs it too.\n- PR titles follow Conventional Commits. Scope rules:\n  - Use the package name (e.g. `feat(server): ...`), or `rpc`/`openapi` for changes tied to a protocol rather than a single package, such as its serializers, handlers, links, or dedicated plugins.\n  - Use `content` only for the docs site itself (styles, Blume config, ...), and only with the `chore` type — never `feat`/`fix`/`perf`/`docs`, since these changes do not affect library users. Documentation about a package or protocol uses that package/protocol scope instead (e.g. `docs(openapi): ...`).\n  - Omit the scope when the change is too broad for a single one.\n\n<!-- File: .agents/skills/blume-migrate/references/docsify.md (Tokens: ~5997 | Category: anthropic-skill) -->\n# Docsify → Blume\n\nDocsify has no build step. One `index.html` sets `window.$docsify = {…}`, loads Docsify and its plugins from a CDN, and renders each page's Markdown in the browser. Watch for five traps:\n\n- **URLs live after a `#`** (`/#/guide/setup?id=install`). A server never sees that part, so `redirects` can't match an old URL. The site needs a client-side redirect script (URLs below).\n- **Links resolve from the docs root**, not the page's folder, unless `relativePath: true`. Images and includes resolve from the page's folder either way, even with a leading `/`. Raw HTML `href`/`src` resolve from the docs root, because the document URL is always the folder holding `index.html`.\n- **Pages have no frontmatter.** The title is usually the first `# H1`, but not always.\n- **Navigation is a Markdown list** (`_sidebar.md`), and its groups usually sit flat in one folder.\n- **Heading ids differ from Blume's**, and old deep links carry them as `?id=`.\n\n**Fingerprint the version.** It changes callouts and heading ids. `docsify@4`, or any script URL with `/lib/` (even an unversioned `…/npm/docsify/lib/docsify.min.js`), runs v4 code: the v5 package still ships the v4 build under `lib/`. `docsify@5/dist/…` or `new Docsify({…})` is v5.\n\n## Detect\n\n- An **`index.html`** whose inline script sets **`window.$docsify`** (v5 can also use `new Docsify({…})`), usually in `docs/`. The folder holding it is the content root.\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `.nojekyll` beside it, with `README.md` as the home page.\n- `docsify-cli` or `docsify` in `package.json`. Without either, the source has to be named: `npx blume migrate docsify`.\n- A GitHub wiki has `_Sidebar.md` and `Home.md` but no `index.html`. It isn't Docsify.\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that runs `docsify serve <dir>` (usually the repo root), so the default `content.root: \"docs\"` matches. With no manifest, scaffold one there.\n\nRead the whole `index.html`: the `$docsify` object (it can be a function), every `<script src>` (each is a plugin), the `<head>`, and the `<body>`. Open a few routes on the live site too. **A page whose include target is missing rendered blank in Docsify 4**, so some \"pages\" never worked.\n\n## Run the codemod first\n\nOn the Docsify folder, from the directory that will hold `blume.config.ts`, dry-run the bundled codemod, then apply it once, before any hand edits:\n\n```bash\ncurl -s https://api.github.com/emojis > emojis.json   # only if pages use :shortcodes:\nnode <skill>/scripts/docsify-codemod.mjs docs\nnode <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old \\\n  --group-folders --emoji emojis.json --site https://docs.example.com docs\n```\n\nIt's zero-dependency and idempotent, and reads `version`, `relativePath`, `routerMode`, `noEmoji`, `homepage`, `loadSidebar`, `loadNavbar`, and `coverpage` from `index.html` (override with `--version` and `--relative-path`). It converts:\n\n- `!>` → `:::warning`, `?>` → `:::tip`, and `> [!TYPE]` alerts → directives, leaving fenced and indented code alone;\n- docsify-tabs → `<Tabs>`;\n- `':include'` lines → `<include>`, resolved from the page's folder as Docsify did, even with a leading `/`. A `:fragment=` is copied to its own file where Docsify cut one: code includes, plus Markdown ones in v5. Docsify 4 showed a Markdown include whole, so it stays whole;\n- `:id=` → `[#id]`, `{docsify-ignore}` → `[!toc]`, and setext headings → ATX;\n- links resolved the way Docsify resolved them (hash links, `?id=`, and `--site` absolute links included) → relative file links, without an anchor that named the target's title H1 (that heading becomes `title`);\n- images → rebased for moved pages. Docsify read a leading-slash image from the page's folder too, where Blume would read it from `public/`;\n- link and image attribute strings → dropped;\n- relative raw HTML URLs and `':ignore'` links → root paths. An `':ignore'` link to a page's own file opened the raw Markdown, so it becomes a raw `<a href>` to the page's Markdown copy (`/<new route>.md`): a Markdown link naming a page's file, even root-relative, lands on the page itself;\n- emoji shortcodes → Unicode;\n- the H1 or sidebar label → `title`, plus `sidebar.label`, `seo.title`, and `hidden: true`, with later H1s demoted;\n- `README.md` → `index.md`, and pages that need MDX → `.mdx` with MDX-safe comments, `<br>`, void tags, and autolinks.\n\n`--group-folders` moves each page into a folder per sidebar group, writes each folder's `meta.ts` in sidebar order plus a root `meta.ts` that keeps the groups in sidebar order, and writes the old → new route table to `docsify-routes.json`. Leave the flag off to keep the files where they are. With `homepage: \"intro.md\"`, the table also maps `/intro` → `/`, since Docsify served that page at both.\n\n`--snapshot` records every page's headings with the ids Docsify gave them, plus `old-routes.txt`. The pin step (URLs below) needs it, and it only works on the first run, while the pages are untouched. On Zenroom it matched Docsify 4.13.1's rendered ids on every page that rendered.\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each. The codemod never invents a fix: a missing include stays as written, and a file that must move to `public/` is only listed.\n\n## Config: `window.$docsify` → `blume.config.ts`\n\n| Docsify | Blume |\n| --- | --- |\n| `name` (may hold HTML) | `title`, as plain text. The `<title>` and `<meta name=\"description\">` in `index.html` give `title` (when `name` is unset) and `description` |\n| `logo` | `logo`, with the file moved to `public/`. Docsify shows the logo instead of the name, so a wordmark needs `logo: { image, text: \"\" }`. Check it in dark mode |\n| `nameLink` | `logo.href` |\n| `repo` | `github: { owner, repo }` (with `branch` from an edit-on-github URL). A non-GitHub repo goes in `navigation.actions` |\n| `themeColor` (v4), `--theme-color` in a stylesheet (v5) | `theme.accent` |\n| A dark-only theme (`dark.css`, a themeable `*-dark.css`) | `theme.mode: \"dark\"`. Other themes map to nothing |\n| Fonts in the stylesheet or `<link>`s | `theme.fonts` |\n| `<link rel=\"icon\">` | Copy the file to `public/favicon.<ext>` |\n| `<meta>` verification tags | `seo.metatags`. `twitter:site` goes to `seo.x.handle`. Drop `og:*` and `twitter:` card tags, since Blume writes them |\n| Custom CSS | A root `theme.css` with only the rules whose markup still exists. Docsify selectors (`.markdown-section`, `.sidebar`, `.cover`) match nothing |\n| Site URL (`CNAME`, a sitemap) | `deployment.site`. GitHub Pages isn't auto-detected |\n| `subMaxLevel: N` | `toc: { maxHeadingLevel: N }` when N ≥ 2 and isn't 3. Drop `maxLevel` |\n| `homepage: \"x.md\"` | Handled by the codemod. A remote URL needs vendoring |\n| `alias` | Local-to-local entries go in the route table. Remote targets: see Includes and aliases. Drop sidebar plumbing (`'/.*/_sidebar.md'`) and any alias whose target doesn't exist |\n| `externalLinkTarget: '_blank'` (explicit) | `markdown: { externalLinks: true }`. Docsify opens external links in a new tab even when this is unset, so report the change |\n| `notFoundPage` / `_404.md` | Blume's 404 page, or a custom `pages/404.astro` |\n| `formatUpdated`, `{docsify-updated}`, a last-modified plugin | `lastModified: \"git\"` (and `fetch-depth: 0` in CI) |\n| `collapsibleSidebarGroups` (v5) | `navigation.sidebar.display: \"group\"`, plus `collapsed: false` in each `meta.ts` that should start open |\n| `vueComponents`, `vueMounts`, `vueGlobalOptions`, `executeScript` | An island per widget (`islands/Name.vue`, `docs/content/islands.mdx`). Report |\n| `markdown` (marked options or a `renderer`) | Drop, and report what the renderer did. Common ones are mermaid (now native) and a `json` pretty-printer: minified JSON includes now render as one line, so fix that in whatever generates them. `breaks: true` has no equivalent |\n| `routes` (virtual routes) | Static ones become pages. Report dynamic ones |\n| `auto2top`, `topMargin`, `maxLevel`, `cornerExternalLinkTarget`, `requestHeaders`, `noCompileLinks`, `sidebarPosition`, `keyBindings`, `skipLink`, the search and pagination options, … | Drop |\n\n**Plugins** are `<script>` tags:\n\n- **Built in:** search, `docsify-copy-code`, `docsify-pagination`, and `zoom-image`. Delete them.\n- **Converted by the codemod:** emoji, `docsify-tabs`, and flexible-alerts.\n- **Prism language packs:** delete them. Shiki highlights the common languages in any case (` ```JSON ` works), and a fence in a language it doesn't know, or a misspelled one, renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: fix the name, or write `text`.\n- **Mermaid** plugins or renderers → native ` ```mermaid ` in `.mdx`.\n- **KaTeX** → `$$…$$` in `.mdx`, with inline `$…$` becoming `$$…$$` inside its sentence.\n- **`ga` / `gtag`** → `googleAnalytics({ id })` for a `G-` id. A `UA-` id no longer collects data, so report it. Other analytics → `script()`.\n- **Comments** (Disqus, Gitalk, giscus) → nothing built in. Use a `PageFooter` slot, or report them.\n- **Anything else** (bibtex, remote-markdown, demo boxes, custom inline plugins) → report.\n\n## Navigation: `_sidebar.md` → folders and `meta.ts`\n\n- **Groups over pages in one flat folder** (everything in `docs/` or `docs/pages/`) flatten silently. Use `--group-folders`: it moves each group's pages into a folder and writes `meta.ts` and the route table, and the hash script maps every old route. This departs from SKILL.md's `(group)/`-first rule: every Docsify URL changes form anyway (`/#/x` → `/x`), so the script runs for each old link either way, and the codemod rebases the moved pages' links and images. To keep URLs instead, move pages at the content root into `(group)/` folders, which add no URL segment; under a subfolder like `pages/`, they keep `/pages/x` but nest every group under a \"Pages\" group. Or keep the files and write an explicit `navigation.sidebar`.\n- **Groups whose pages already sit in a folder named for the group** (`Guide` → `guide/`) stay put, and `--group-folders` writes that folder's `meta.ts`. It moves the pages of a group whose folder has another name into one named for the group, so on a site already laid out in folders, leave the flag off and write each `meta.ts` by hand, with the group label and the sidebar's order.\n- **The home page** stays the root `index`. The generated sidebar lists it first, never inside a group.\n- **Loose pages listed after groups** can't keep that order, because Blume lists loose pages first. Move them into a folder, or report the change.\n- **External links, and links to files** like a PDF or HTML in `public/`, become `navigation.featured`. A file link there warns at build (\"no page matches it\"); the warning is harmless.\n- **A `_sidebar.md` inside a folder** replaced the sidebar under that folder. Map the folder to a `navigation.tabs` entry.\n- **Pages the sidebar doesn't list** get `hidden: true` from the codemod. They stay reachable, as before.\n- **Sidebar link titles** (`[x](x.md \"Tab title\")`) become `seo.title`. Report any that look copy-pasted from another page: Docsify showed them as the browser tab title.\n\n## Navbar and cover page\n\n- **`_navbar.md`:** doc-section links become `navigation.tabs` (a nested list becomes a tab with `items`), and other links become `navigation.actions`. With `mergeNavbar: true`, put the other links in `navigation.featured` instead, not in both: featured shows on every screen size, while actions hide on phones. Language links (`/zh-cn/`) become `i18n`.\n- **`_coverpage.md`** (shown above `README.md` on `/`; the whole home page with `onlyCover: true`): rebuild it as the top of `index.md`. The `# name <small>1.2</small>` heading is already the title. The tagline is prose or `description`, the logo is a Markdown image, and the buttons become links or `<Card>`s. Drop the background image or `![color](#…)`, and report it. Use `mode: center` when the cover was the whole page.\n\n## Content: what the codemod leaves to you\n\n**Titles.** Docsify drops only the first H1 from its sidebar headings, so pages often have several. The codemod takes a leading H1 as the title and demotes every later heading from the second H1 on. When content comes before the first H1, it takes the sidebar label as the title and demotes everything. Check every title it flags: about one page in five on Zenroom started with a section (\"Key generation\", \"Intro\"). For those, use the sidebar label as the title, put the H1 back as an `##` section, and demote every heading.\n\n**MDX hazards** in a renamed page. The codemod lists each one, and all but the first fail the build:\n\n- An indented code block renders as a plain paragraph: fence it.\n- An HTML table whose cell tags share a line with text: give each `<tr>`/`<td>` its own line.\n- A block element inside `<p>`, typical of README banners: make the `<p>` a `<div>`.\n- A repeated attribute: keep one.\n- A brace in prose: escape it as `\\{`.\n\n**Includes and aliases.**\n\n- **A missing include target:** the page was blank on the old site. Repoint the include to the file's new name (a folder was usually renamed), or drop it, and report it.\n- **A fragment copied from a generated file** goes stale. Have the generator write one file per fragment.\n- **An included file's own H1** becomes a second `<h1>`: remove it.\n- **An alias to a raw URL of a file in the same repo** (`'/CHANGELOG': 'https://raw.githubusercontent.com/<owner>/<repo>/main/CHANGELOG.md'`) becomes a page that `<include>`s the file through a symlink in `_snippets/`. Removing that file's H1 changes a file people read on GitHub, so say so.\n- **Other remote aliases and includes:** vendor a copy (with a source comment), or use `mdxRemote()`.\n- **iframe, video, and audio embeds** become raw HTML, with the file in `public/`.\n\n**Files the pages link** (listed under \"must be served from public/\"): Docsify served every file in its folder at its path. Blume publishes a file a page links relatively with that page, but at a new URL, and the codemod writes Docsify's root-resolved links as root paths, which keep the old URLs and are served from `public/`.\n\n- Copy each file into `public/` at the same path.\n- If a generator writes the files (test fixtures, examples), symlink the folder instead: `public/_media/examples → ../docs/examples`. Astro copies the target, and `validate` checks the links.\n- An HTML document saved as `.md` (generated API docs) goes to `public/` as HTML.\n- `blume validate --strict` reports a missing one as `BLUME_BROKEN_ASSET`.\n\n**Everything else:**\n\n- **Link attributes** the codemod dropped (`:target`, `:size`, `:no-zoom`, `:class`): restore any that matter as raw HTML (`<img src=\"/x.png\" width=\"200\" data-no-zoom />`, with the file in `public/`).\n- **Emoji shortcodes left over:** paste the emoji.\n- **Page `<script>`s:** Docsify 4 ran them only with `executeScript` or Vue, so they often never ran. Move one that did into an island.\n- **Vendored READMEs** keep Blume's ids (they had GitHub-style ids already), and they aren't in the snapshot.\n\n## URLs and redirects\n\n**Hash mode (the default).** Every page lived at `/#/<route>`, also reachable as `/#/<route>.md` and `/index.html#/<route>`, with folder indexes at `#/<folder>/` and headings at `?id=<id>`. The server only saw `/`.\n\n**A. Blume takes over the old address** (the usual case). `docsify-routes.json` holds the page moves the codemod made. Add an entry for each alias you turned into a page. Routes that now land on a file in `public/` go in a separate `files` map that only the script reads: a redirect page would sit where the file is. Name a `public/` folder with its slash (`/api/`) or name the file itself (`/api/index.html`): static hosts, `blume dev`, and `blume preview` serve its `index.html` at `/api/` (and redirect `/api` there), where its relative links resolve.\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport moved from \"./docsify-routes.json\" with { type: \"json\" };\n\n// Old routes that now land on a file in public/: the script sends readers\n// there, but a redirect can't, so they stay out of `redirects`.\nconst files = {}; // e.g. { \"/onepage\": \"/onepage.md\" }\n\n// Docsify served every page at /#/<route>; a server never sees the hash.\n// Swap an old hash URL for its new route, and ?id=<heading> for #<heading>.\nconst base = \"\"; // the site's deployment.base plus basePath, if it has either\nconst docsifyRedirects = `(() => {\n  const base = ${JSON.stringify(base)};\n  const routes = ${JSON.stringify({ ...moved, ...files })};\n  const go = () => {\n    const hash = location.hash;\n    if (!hash.startsWith(\"#/\")) return;\n    const q = hash.indexOf(\"?\");\n    let path = q === -1 ? hash.slice(1) : hash.slice(1, q);\n    const id = q === -1 ? null : new URLSearchParams(hash.slice(q + 1)).get(\"id\");\n    try { path = decodeURI(new URL(path, \"http://x\").pathname); } catch {}\n    path = path.replace(/\\\\.md$/i, \"\").replace(/(^|\\\\/)README$/i, \"$1\");\n    if (path.length > 1) path = path.replace(/\\\\/+$/, \"\");\n    // location.origin keeps a crafted #//other.host/ link on this site.\n    location.replace(location.origin + base + (routes[path] ?? path) + (id ? \"#\" + id : \"\"));\n  };\n  go();\n  addEventListener(\"hashchange\", go);\n})();`;\n\nexport default defineConfig({\n  redirects: Object.entries(moved).map(([from, to]) => ({ from, to })),\n  integrations: [\n    {\n      name: \"docsify-hash-redirects\",\n      hooks: {\n        \"astro:config:setup\": ({ injectScript }) => {\n          injectScript(\"head-inline\", docsifyRedirects);\n        },\n      },\n    },\n  ],\n});\n```\n\nThe script runs from every page's `<head>`, the 404 page included, before the body loads, in `blume dev` and in every build. Keep its two safeguards: `new URL(…).pathname` resolves the `./` and `../` segments Docsify left in links (`#/./guide`), and `location.origin` stops a crafted `#//other.host/` from sending readers off the site. Unmoved routes need no entry. The `redirects` also cover direct visits to a moved path and its `/old.md` Markdown twin. Don't use the `script()` analytics adapter for this: it's emitted in production builds only, and held until consent when `consent` is set.\n\n**B. The old site keeps its own address** (an old GitHub Pages URL, while the docs move to a new domain). Replace the old `index.html` with a page running the same parsing, with the new origin in place of `location.origin`, and a `<noscript><meta http-equiv=\"refresh\" content=\"0; url=https://new.example/\"></noscript>` fallback.\n\n**History mode** (`routerMode: 'history'`). URLs were real paths, so the route table and `redirects` carry moved pages. Anchors were still `?id=`, so add this after `go();`:\n\n```js\nif (!location.hash) {\n  const id = new URLSearchParams(location.search).get(\"id\");\n  if (id) location.replace(location.pathname + \"#\" + id);\n}\n```\n\n**Keep old heading anchors.** Docsify 4 leaks entity names into ids (`Foo & Bar` → `foo-amp-bar`, `Don't` → `don39t`), collapses dash runs, keeps emoji and shortcode names, and prefixes a leading digit with `_`. Docsify 5 is closer to Blume, but still differs in that prefix, in non-ASCII capitals, and where a heading has emphasis (`_When_` stays in the id) or a shortcode. After the first build, pin each old id where it differs, and rebuild until it reports 0:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json --write\n```\n\nOn Zenroom, one pass kept every old id except those of title H1s (58 of 968), and a link to one of those lands at the top of the page anyway. A page that never rendered on the old site (a missing include) has nothing to pin.\n\n**Check every old route** against `blume preview`. In `.docsify-old/old-routes.txt`, a folder route is listed without its slash, but Docsify served it only with one. Both forms reach the script.\n\n```bash\nnode -e '\nconst routes = { ...require(\"./docsify-routes.json\") };   // add the files map too\nconst old = require(\"fs\").readFileSync(\".docsify-old/old-routes.txt\", \"utf8\").split(\"\\n\").filter(Boolean);\n(async () => { for (const r of old) {\n  const res = await fetch(\"http://localhost:4321\" + (routes[r] ?? r));\n  if (res.status !== 200) console.log(res.status, r);\n} })();'\n```\n\nThen open a few old hash URLs, with and without `?id=`, in a browser on the deployed site. A plain `curl` can't test the hash script.\n\n**GitHub Pages**, where most Docsify sites live:\n\n- Switch the Pages source from the branch folder to GitHub Actions, using the workflow in https://useblume.dev/guides/markdown-docs-github-pages (with `fetch-depth: 0` for `lastModified: \"git\"`).\n- Pages has no redirect rules of its own. A static build writes each exact `redirects` entry as a page that forwards to the new URL, which is all the table holds; a pattern redirect wouldn't work there.\n- A project URL (`user.github.io/repo/`) becomes `deployment: { site: \"https://user.github.io\", base: \"/repo\" }`, and that base goes in the script too. Blume adds the base to root paths in Markdown and raw HTML alike, so the codemod's root paths need nothing more.\n- The custom domain stays in the Pages settings. Neither `CNAME` nor `.nojekyll` is needed.\n- The workflow's `npm ci` needs a lockfile, so commit one.\n\n## Teardown\n\nAfter harvesting what they declare, delete:\n\n- `index.html`;\n- `_sidebar.md`, `_navbar.md`, `_coverpage.md`, and `_404.md` (Blume ignores `_` files meanwhile), or the files `loadSidebar`, `loadNavbar`, and `coverpage` name instead. Without the `_`, Blume publishes them as pages until they're gone;\n- `.nojekyll`, the theme CSS, and generators that read the sidebar or Docsify syntax (sitemaps, one-page builds).\n\nSwap `docsify-cli`/`docsify` for `blume` and `docsify serve` for `blume dev`/`blume build`/`blume preview`. Keep `docsify-routes.json` and `.docsify-old/` until the pins are done and old links stop mattering.\n\n## Dropped — report these\n\n- The cover's background and the navbar's language links (now `i18n`).\n- The home page's place in a sidebar group, and loose pages after groups.\n- Link and image attributes you didn't restore.\n- Fragments now frozen as copies.\n- Vendored or `mdxRemote()` aliases.\n- Pages that were blank on the old site (missing includes) and what you did with each.\n- Title H1 anchors (they land at the page top).\n- Comment plugins, a `UA-` analytics id, Vue widgets, and page scripts.\n- Custom `markdown` renderers (JSON pretty-printing included) and `breaks: true`.\n- Dynamic virtual `routes`.\n- Theme CSS with no Blume target, and copy-pasted sidebar link titles now in `seo.title`.\n- New-tab external links, unless you set `markdown.externalLinks`.\n\n<!-- File: .agents/skills/blume-migrate/references/docus.md (Tokens: ~7574 | Category: anthropic-skill) -->\n# Docus → Blume\n\nDocus is a Nuxt layer for docs sites. It's built on Nuxt Content (pages are Markdown files in `content/`, routed by their path) and Nuxt UI (the theme, and the prose components pages use). Navigation is the file tree plus per-folder `.navigation.yml` files, and settings live in `app.config.ts` and `nuxt.config.ts`. Pages are written in **MDC**, Nuxt Content's Markdown-with-components syntax. The bundled codemod converts most of it; these traps are why it exists:\n\n- **MDC's `::note` … `::` looks like a Blume directive and isn't one.** Blume prints `::note` and its `::` closer as text (in `.md`, every MDC construct), and `blume check` and `build` warn `BLUME_MDC_SYNTAX` at each block opener and inline component (`:badge[New]{color=\"primary\"}`) left in either format: convert them as below.\n- **MDX is stricter than MDC.** `[text]{.class}` spans fail the build, and so do `{…}`, `<Name>`, and `<50` in prose. Prompt bodies are full of these.\n- **Icons are Iconify names** (`i-lucide-zap`, `i-simple-icons-github`). Blume renders nothing for them, and warns only for sidebar and tab icons.\n- **Docus sorts siblings by file path as text,** so `10.faq.md` lists between `1.` and `2.`. Blume sorts the prefixes as numbers.\n\nScope:\n\n- **Docus 3.x–5.x** is covered: the `docus` package on Nuxt Content 3 and Nuxt UI, whose content syntax is the same across the three. 5.x is current and is by far the most installed, and it's the one this reference was run on.\n- **Docus 1.x** (`@nuxt-themes/docus`, on Nuxt Content 2 with Nuxt Elements components) is flagged. Its section near the end was written from the package source, not tested on a site, and the codemod doesn't convert its components.\n- **The Nuxt UI docs template** (`nuxt-ui-templates/docs`, no `docus` dependency) writes the same content: the codemod and this reference apply, and its `app.config.ts` is read by hand.\n\n## Detect\n\n- **A `docus` dependency** means 3.x–5.x, and **`@nuxt-themes/docus`** means 1.x. It's usually in the docs package's own `package.json` (`docs/`, `apps/docs/`) in a module's monorepo.\n- Usually a `nuxt.config.ts` with `extends: ['docus']`. The 4.x starters have no config and run `nuxt dev --extends docus`, and 3.x ran `docus dev`.\n- A `content/` folder beside it with `.navigation.yml` files (3.x+) or `_dir.yml` files (1.x).\n\n**Where the Blume project goes.** Put `blume.config.ts` beside the `package.json` that depends on `docus`, and set `content.root: \"content\"`. `public/` stays where it is: both tools serve it at the site root. In a workspace, read `references/monorepo.md` too.\n\n- **No `content/docs/` folder:** every file under `content/` is a page, and `content/index.md` is the landing page at `/`.\n- **A `content/docs/` folder:** Docus publishes only `content/docs/**`, under `/docs/…`, plus `content/index.md`. Exclude anything else under `content/` and report it. Docus hides the `docs` level in its sidebar. In Blume, add a tab `{ label: \"Docs\", path: \"/docs\" }`, or the section tabs below, or the sidebar wraps everything in a \"Docs\" group.\n- **`content/<locale>/` folders:** see i18n.\n\n## Record the old site first\n\nBefore editing anything, save the URL list, the old heading ids, and the record of what rendered:\n\n- **URLs:** the live `/sitemap.xml` (on an i18n site, an index of one sitemap per locale), plus every `routeRules` redirect source in `nuxt.config.ts`.\n- **HTML:** `nuxt generate` writes `.output/public`, one `<route>.html` per page plus a meta-refresh stub per redirect. Copy it outside the repo; it's `<old build>` below. Or point the anchor script at the live origin.\n- **Check components against the live site.** A local build can miss a component a Nuxt module registers in its own build: it shows as an empty tag (`<InstallButton …></InstallButton>`) while production renders it. Request live pages with a browser user agent (`curl -A 'Mozilla/5.0'`): some Docus deployments answer `curl` with Markdown.\n\n## Run the codemod first\n\nOn the `content/` directory, dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/docus-codemod.mjs content          # report only\nnode <skill>/scripts/docus-codemod.mjs --write content  # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent. Icon names are checked against Blume's Lucide set once `blume` is installed (or with `--lucide <icons.json>`).\n\n**What it does:**\n\n- **Pairs MDC blocks** as remark-mdc does (see MDC syntax).\n- **Converts the Nuxt UI prose components:**\n  - callouts → `:::type` directives, or `<Callout type icon>` when a callout's icon isn't its type's own\n  - `card-group`/`card`, `field-group`/`field` → `<ResponseField>`, `steps`, `tabs`, `accordion`, `collapsible`\n  - `code-group` → `<CodeGroup>`, or one `package-install` fence when the group only repeats an install command\n  - `code-collapse`, `code-tree`, `code-preview`, `prompt`, `badge`\n  - the inline `:icon`, `:kbd`, `:badge`, `:video`, and `:u-color-mode-image`\n- **Strips Markdown attributes:** `[text]{.class}` spans and `{…}` attributes go.\n- **Rewrites fence labels and frontmatter:** fence `[label]`s become titles, `navigation` → `sidebar`, `seo.ogImage` → `seo.image`, `links` → `related`, a body H1 → `title`, and `.navigation.yml` → `meta.ts`.\n- **Renames to `.mdx`** each page that now needs MDX, and makes its prose MDX-safe (see MDC syntax).\n\nEvery `REVIEW` line is your to-do list, including:\n\n- custom and landing-page components, left as written\n- blocks with a slot the conversion doesn't read (a card's `#footer`), left as written\n- card descriptions, which Nuxt UI's prose card never rendered\n- callouts that linked somewhere\n- icons outside Lucide\n- unclosed blocks and stray closers\n- `links` lists longer than `related` takes, and frontmatter keys Blume rejects\n- MDC blocks inside blockquotes\n- file names whose URL changes\n\n## Config: `app.config.ts` → `blume.config.ts`\n\nRead `app/app.config.ts` (`app.config.ts` at the project root on Nuxt 3), `nuxt.config.ts`, the CSS that `css` lists, and `.env.example`.\n\n| `app.config.ts` | Blume |\n| --- | --- |\n| `seo.title` | `title`. Docus falls back to `site.name` in `nuxt.config.ts`, then the `package.json` `name`, then the git repository's name, so carry the one the site showed |\n| `seo.description` | `description` (Docus falls back to the `package.json` `description`) |\n| `seo.titleTemplate` | drop. The default `%s - <site>` is Blume's; report a custom one |\n| `seo.schema` | drop (report). Blume writes its own JSON-LD |\n| `header.title` | `title`, when there's no logo |\n| `header.logo.{light,dark,alt}` | `logo: { image: { light, dark, alt }, text: \"\" }`. Docus shows the logo without the title. A monochrome SVG → `currentColor` and the string form (SKILL.md) |\n| `header.logo.display: 'wordmark'` | the `wordmark` files as the logo |\n| `socials` (Simple Icons slug → URL) | `footer.socials` for the platforms Blume lists (`twitter` → `x`). Others (`nuxt`, `npm`) → `footer.links`, or drop (report) |\n| `github` (`url`, `branch`, `rootDir`), or nothing | `github: { owner, repo, branch, dir }`. **Docus reads the git remote when `github` isn't set,** so edit links showed anyway: take `owner`/`repo` from the remote. `dir` = `rootDir`; `branch` only when it isn't `main`. `github: false` → leave it out |\n| `toc.bottom.links` | `navigation.featured` (`to` → `href`). Report the move |\n| `navigation.sub` | `navigation.tabs` (see Navigation). `'aside'` anchors have no Blume form; report |\n| `docus.locale` (not `en`) | a one-locale `i18n` block |\n| `docus.colorMode` | `theme.mode`. Blume always shows the toggle; report |\n| `assistant.faqQuestions` | `ai.assistant.suggestions` (`[{ label }]`; flatten categories) |\n| `ui.colors.primary` | `theme.accent`. Nuxt UI paints shade 500 in light mode and 400 in dark: a Blume preset of the same name, else `{ light, dark }` with those Tailwind values. **Not declared means Docus's emerald:** the site turns Blume's default color unless you offer `theme.accent: \"green\"`; report it either way |\n| `--ui-radius: 0` in CSS | `theme.radius: \"none\"` |\n| `toc.title`, `docus.shortcuts`, `search.fts`, other `assistant.*` and `ui.*` keys | drop (report the `ui` overrides) |\n\n| `nuxt.config.ts` | Blume |\n| --- | --- |\n| `site.url` / `NUXT_SITE_URL` | `deployment.site`, unless the host is Vercel or Netlify (SKILL.md) |\n| `routeRules` with `redirect` | `redirects` (see URLs and redirects) |\n| `@nuxtjs/i18n` and `i18n` | see i18n |\n| `llms.notes`, `llms.sections` | `agents.llmsTxt: { details }` as Markdown, if worth keeping; `llms.title`/`description` drop |\n| `mcp` | Docus serves an MCP server at `/mcp` (or `mcp.route`) unless `mcp: false` or `mcp.enabled: false`. To keep one: `agents.mcp: { enabled: true, name }` (and `route` when it isn't `/mcp`) plus a host adapter (server output). Blume's tools are fixed, so report the site's own tools and prompts in `server/mcp/` |\n| `docus.assistant` | Docus turns the assistant on when the build has `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`: check the live site for it. Keep it as `ai: { assistant: { enabled: true, provider: gateway({ model }) } }` (`gateway` from `blume/ai`; Docus's default model is `google/gemini-3-flash`) plus a host adapter, or drop it (report) |\n| `app.head`, `@vercel/analytics`, `@nuxtjs/plausible`, `@nuxt/scripts` analytics | the provider's factory from `blume/analytics` (`vercel()`, `plausible({ domain })`, `cloudflare({ token })`); other scripts → `script()`; `meta` → `seo.metatags`. `@vercel/speed-insights` has no adapter (report) |\n| `content.build.markdown` (`toc`, `highlight.theme`, plugins), `mdc.highlight.theme` | `toc` and `markdown.code.theme` only when they differ from the defaults; each remark/rehype plugin's syntax converted by hand (report) |\n| `css` | what still applies → a root `theme.css` against Blume's tokens |\n| `studio`, `agentDiscovery`, `ogImage`, `sitemap`, `robots`, `schemaOrg`, `image`, `icon`, `nitro`, `vite` | drop (report). Blume writes its own sitemap, robots, OG images, and JSON-LD |\n\n**A project `content.config.ts`** adds collections such as a blog or changelog. Those aren't docs pages: map a blog to `type: blog` and a changelog to `type: changelog` (or `githubReleases()`, SKILL.md Changelogs), and report anything else.\n\n**Theme overrides** (`AppHeader*.vue`, `AppFooter*.vue`, `Docs*.vue`) aren't content. Map what they add to config (`navigation.cta`, `navigation.actions`, `footer.links`, `banner`), but read their `v-if`s first: a button shown only on the landing page (`route.path === '/'`) belongs on the landing page, since Blume's `cta` shows site-wide.\n\n## Navigation\n\nThe codemod writes each `.navigation.yml` as a `meta.ts` (`title`, `icon`) and each page's `navigation` as `sidebar` (`title` → `label`, `icon`, `false` → `hidden`). What's left:\n\n- **A folder's `navigation: false`** hides it: set `sidebar.hidden: true` on its pages (the codemod reports it).\n- **A folder with an `index.md` but no `.navigation.yml`:** Docus labels it with the index page's title. Blume uses the humanized folder name, so set `meta.ts` `title` when they differ.\n- **Order:**\n  - Docus compares paths as text: `1.intro`, `10.faq`, `11.glossary`, `2.setup`. Blume sorts the numbers. In a folder with ten or more unpadded siblings, keep Blume's order (almost always what the author meant) and report the change.\n  - Docus also interleaves pages and subfolders. Blume lists a level's pages above its subfolders, so report a page that sorted after a folder.\n- **Display:** Docus draws every folder as an always-open group, which is Blume's default `flat` display.\n- **`navigation.sub`** (a header tab bar, or anchors in the sidebar) makes each top-level folder a section that scopes the sidebar. That's `navigation.tabs`: one `{ label, path, icon? }` per top-level folder with pages, with `label` and `icon` from its `.navigation.yml`. A section with no index page needs no `href`, since a Blume tab falls back to the section's first page, as Docus did.\n\n## Pages: frontmatter and titles\n\nThe codemod maps `navigation`, `links` (header buttons → `related` cards at the foot of the page), and `seo.ogImage`. It removes `layout: docs` and `sitemap`. What's left:\n\n- **No `description`:** when a page opens with a paragraph (after an optional `# H1`), Docus took it as the description, so it showed twice. Blume derives none. To keep the meta description without the repeat, set `seo.description` to that paragraph (optional; say which you did).\n- **`sitemap: false`** has no sitemap-only switch: add `seo: { noindex: true }` only when the page should also leave search engines.\n- **`toc: false`** → `mode: wide` (report), or drop. Another `layout` → a Blume `mode`.\n- **Other keys** Nuxt Content kept and Blume rejects: drop and report each one, or declare it with `frontmatter.extend` if a component you wrote reads it.\n- **Titles:** Docus renders frontmatter `title` in the page header, falling back to the first `# H1`, so a body H1 showed twice. The codemod moves a lone H1 into `title` and reports the rest.\n\n## MDC syntax\n\nThe codemod applies these rules; you need them for what it leaves. They're remark-mdc's behavior (checked against `remark-mdc@3.11.1`):\n\n- **Block component:** an opening line `::name`, optionally followed by `{attributes}` and nothing else, then Markdown, then a closing line of colons only.\n  - Props are inline (`{key=\"value\" flag .class #id}`) or a YAML block, from `---` right after the opener to the next `---`. A `:key='[…]'` holds JSON.\n  - Slots are `#name` lines inside the block.\n  - An inline component is `:name[text]{attrs}`; alone on a line, it renders as a block.\n- **Pairing:**\n  - Every opener nests, even at the same colon count.\n  - A closer closes the innermost open block **whose opener has exactly as many colons**, together with anything still open inside it. It must not be indented deeper than that opener. Any other colon line, or a `::` mid-line, is text.\n  - An unclosed block runs to the end of its parent, and a closer inside a code fence is code.\n  - Check the old HTML for a literal `::` to see what the live site already showed broken.\n- **MDX safety:** in a `.mdx` page, prose must escape `{` and `}` as `\\{` and `\\}`, and turn **any `<` followed by a non-space character** into `&lt;` (or `\\<`) unless it opens a real tag: `useFetch<User>()`, `<script setup>` written as text, `<50 ms`, `<= 5`. Case matters as in JSX: `<input>` is HTML, but `<Input>` is a component, so a generic like `useTool<Input>()` is text. HTML comments become `{/* … */}`, `<https://…>` autolinks become links, and `<br>` becomes `<br />`. Code spans and `$$` math stay as written.\n- **Code fences:** ` ```ts [file.ts]{2} ` → ` ```ts file.ts {2} ` (`title=\"Two words\"` for a label with spaces). Blume reads ` ```ts [file.ts]{2} ` and a multi-word title as written too, the range highlighting its lines, so this only tidies. Leave MDC samples inside fences alone: they're documentation.\n\n## What the codemod leaves\n\n- **Custom components:** `app/components/content/*.vue`, or a Nuxt module's. Read the SFC to see what it renders and which props and slots it reads, then convert it:\n  - to Markdown\n  - to a Blume component\n  - to a static `.astro` component registered in `components.ts`\n  - to the `.vue` file as an island in `islands/`, with `@astrojs/vue` and `vue` installed. `:items='[\"a\"]'` becomes `items={[\"a\"]}`, and named slots become props or children.\n\n  Report each one. A component that rendered nothing on the live site (an unknown tag) has nothing to convert.\n\n- **Callouts with `to`:** the whole callout was a link. Add the link to its text when it isn't there already.\n- **Card descriptions:** Nuxt UI's prose card renders neither a `description` prop nor a `#description` slot, so that text never showed. The codemod keeps it as the card's body and flags it: keep it, or delete it to match the old page.\n- **Blocks with a slot the conversion doesn't read** (a card's `#footer`, a tab's extra slot) stay as written: convert them by hand.\n- **`code-tree`** becomes a `<CodeGroup>` titled by file path. The tree view is gone; a `<FileTree>` before it can show the layout.\n- **`prompt`:** Blume has no `windsurf` or `claude` action, and no prompt icon (the codemod reports both).\n- **Prose about features that didn't carry over:** grep the content for the dropped MCP tool and prompt names, `docus`, `/.well-known/skills`, and `Studio`, and list every page that now says something false.\n\n## Landing page\n\n`content/index.md` renders full-width, with no sidebar, title, or table of contents. Its `title` and `description` feed only the meta tags. The codemod leaves its `u-page-*` and custom components as written.\n\n- **Markdown landing** → `index.mdx` with `mode: custom` (no title or sidebar rendered, so write the heading) and `sidebar.hidden: true` (Docus never listed it).\n- **A designed landing**, or `app/pages/index.vue` → `pages/index.astro` on `PageLayout`.\n\n| Nuxt UI | Blume, in the `mode: custom` page |\n| --- | --- |\n| `u-page-hero` (`title`, `description`, `links`, or slots) | `# Title`, a paragraph, and a line of Markdown links; the primary one can also be `navigation.cta` |\n| `u-page-section` | `## Title`, a paragraph, and its features as cards |\n| `u-page-feature`, `u-page-card` (`title`, `description`, `icon`, `to`) | `<Card title icon href>description</Card>` in `<CardGroup cols={3}>` |\n| `u-page-grid`, `u-page-columns` | `<CardGroup cols={3}>` |\n| `u-button`, `u-page-cta` | Markdown links; a heading and a paragraph |\n| `u-page-logos`, decorative `div`s (glows, gradients) | a row of images, or drop (report) |\n\n## Icons\n\nBlume resolves Lucide names only (`zap`, or `lucide:zap`); **anything else renders nothing.** The codemod converts `i-lucide-*` (aliases of renamed icons included) and reports the rest:\n\n- **Other sets** (`i-heroicons-*`, `i-ph-*`): the closest Lucide name, checked at [lucide.dev/icons](https://lucide.dev/icons).\n- **Brands** (`i-simple-icons-*`): Lucide has a few (`github`, `gitlab`, `slack`, `linkedin`, `youtube`), but **`i-simple-icons-x` is not Lucide's `x`, a close cross.** Otherwise, save the SVG under `public/` and pass its path (`icon=\"/icons/nuxt.svg\"`), or drop it (report).\n- **`i-custom-*`** (Docus's `app/assets/icons/*.svg`): copy the SVG to `public/icons/` and pass the path.\n\n## i18n\n\nDocus i18n is `@nuxtjs/i18n` (`defaultLocale`, `locales: [{ code, name }]`) plus one `content/<code>/` folder per locale, and it forces the `prefix` strategy: every locale is under its code, the default included, and `/` redirects to the default.\n\n- Move `content/<default>/` up to `content/` (a default-locale folder warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`). Keep the others as `content/<code>/`, and run the codemod on `content/`.\n- Write `i18n: { defaultLocale, locales: [{ code, label }], hideDefaultLocalePrefix: false }`, `label` = `name`. That keeps every `/en/…` URL. Nothing serves `/` then, so add `{ from: \"/\", to: \"/<default>\", status: 302 }` as Docus did.\n- **Regional codes are lowercase in Docus URLs** (`zh-TW` → `/zh-tw/…`): use the lowercase code as Blume's `code` and folder.\n- Leave out any configured locale with no `content/<code>/` folder: Docus skipped it.\n\n## URLs and redirects\n\nDocus routes a page by its path under `content/`. It strips an `N.` prefix from each segment, drops `index`, and slugifies (lowercase, spaces → `-`). Blume strips `N.`, `N-`, and `N_` prefixes and `index` the same way, so **most URLs carry over unchanged**, with no trailing slash or `.html` to redirect. The codemod reports the exceptions:\n\n- **A dash prefix** (`01-intro.md`) stays in the Docus URL and Blume strips it: pin `slug` or redirect.\n- **Uppercase letters or spaces** were lowercased by Docus: rename the file.\n- **A version name** (`1.x`, `2.0`) stays whole in Docus: pin `slug`.\n- **`_` files** are pages in Docus and excluded by Blume: rename them.\n\n**Redirects:**\n\n- **`routeRules` → `redirects`.** A string `redirect` is a 307 in Nitro (`status: 307`); `{ to, statusCode }` → `{ to, status }`. A `/**` key strips its base: `'/old/**': { redirect: '/new/**' }` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`.\n- **`/raw/<route>.md`** is the Markdown copy of each page, which Docus links from `llms.txt`. Blume serves it at `/<route>.md`, so one rule covers it: `{ from: \"/raw/:path*\", to: \"/:path*\" }`. A `routeRules` redirect between two `/raw/…md` paths should target Blume's copy directly (`/raw/old.md` → `/new.md`), or it chains through that rule.\n- **Agent skills:** a `skills/` folder Docus published under `/.well-known/skills/` → `agents.skills: \"./skills\"`. Blume publishes them under `/.well-known/agent-skills/`, a skill with references as one `.tar.gz`. Redirect the old index to the new one, and report the change. Links can name the new index (`/.well-known/agent-skills/index.json`): `blume validate` accepts the files Blume generates.\n- **Anchors:** MDC slugs headings like Blume (`github-slugger`), then collapses repeated dashes, trims edge dashes, and prefixes a leading digit with `_`. `## Name & Title` is `#name-title` in Docus and `#name--title` in Blume; `## 1. Install` is `#_1-install`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old <old build>`, `--write`, and rebuild until it reports 0. On a server build (`vercel()`, `node()`) it reads the pages from `dist/client` itself.\n- **Verify** every old URL: a built page, or a redirect `from` whose target is built (SKILL.md Verification).\n\n## Deploy and teardown\n\n- **Keeping the assistant or the MCP server** needs server output: `deployment: vercel()` (or `node()`, `netlify()`, `cloudflare()`). `blume preview` can't serve a `vercel()` build, so check it with `blume dev`, then a preview deployment.\n- **Vercel with `vercel()`:**\n  - The build writes the Build Output API folder `.vercel/output` in the Blume project, not `dist/`.\n  - In the project's `vercel.json`, set `\"framework\": null` and the install and build commands (`references/monorepo.md` §4 for a workspace), and **no `outputDirectory`**. `framework: null` overrides a project still set to the Nuxt preset.\n  - In the dashboard, set the Root Directory to the docs package and Node to 22.19 or later. Set `AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token.\n  - With Turborepo, add `.vercel/output/**` to the docs' build outputs.\n  - List the dashboard settings as steps for the user.\n- **Dependencies:** remove `docus`, `nuxt`, the `@nuxt/*`/`@nuxtjs/*` modules, `nuxt-studio`, `better-sqlite3`, `@iconify-json/*`, `tailwindcss` (if only the theme used it), mapped analytics packages, `@nuxt/eslint-config`, and `vue` (keep it with `@astrojs/vue` for islands). Keep what the site's tests or scripts still import.\n- **Scripts:** `dev`/`build`/`generate`/`preview` → `blume dev`/`build`/`build`/`preview`. **Delete `postinstall: nuxt prepare`**, which fails once Nuxt is gone.\n- **Files:** after harvesting, delete `nuxt.config.ts`, `app/`, `content.config.ts`, `server/` (report its routes), and a Nuxt `eslint.config.mjs`. The codemod leaves a `.navigation.yml` whose keys `meta.ts` can't hold.\n- **`tsconfig.json`:** a Nuxt one points into `.nuxt/`, and `blume build` then fails with `BLUME_TSCONFIG_EXTENDS`. Delete or replace it.\n- **`.gitignore`:** in the docs package's own file, `.nuxt`, `.output`, `.data` → `.blume`, `dist`.\n- **CI:** grep for `nuxt generate`, `nuxi`, `.output/public`, and `--extends docus`.\n\n## Docus 1.x (`@nuxt-themes/docus`)\n\nWritten from `@nuxt-themes/docus@1.15.1` and `@nuxt-themes/elements@0.9.5`. Everything above applies (run the codemod for fences, frontmatter, and the H1), with these differences:\n\n- **Config** is `app.config.ts` → `docus: { … }`:\n  - `title`, `description` → same keys\n  - `url` → `deployment.site`\n  - `socials` → `footer.socials`. Values are handles (`twitter: '@nuxt_js'`, `github: 'owner/repo'`), so build each URL.\n  - `github: { owner, repo, branch, dir }` → `github`\n  - `header.logo`/`title` → `logo`/`title`\n  - `aside.collapsed` → `navigation.sidebar.display: \"group\"`\n  - `footer.textLinks`/`iconLinks` → `footer.links`\n  - everything else → drop (report)\n- **Folders** use `_dir.yml` (`title`, `icon`, `navigation: false`, `navigation.redirect` → a redirect from the folder's route).\n- **Titles** are the body `# H1` (the codemod moves it).\n- **Frontmatter:**\n  - `layout: page` → `mode: custom` or `mode: center`\n  - `aside: false` → `mode: center`\n  - `toc: false` → `mode: wide`\n  - `redirect` → a redirect\n  - `bottom`, `header`, `fluid`, `main` → drop\n- **Components** (Nuxt Elements, left as written and reported):\n  - `::alert{type}` → that directive (`primary` → `tip`)\n  - `::callout{type}` with `#summary`/`#content` → the directive holding the summary, then `<Expandable title=\"Details\">` around the content\n  - `::list{type}` → a plain list\n  - `::badge{type}` → `<Badge>`\n  - `::button-link{href}` → a link\n  - `::terminal{content}` → a `bash` fence\n  - `::card-grid` + `::card{icon}` with `#title`/`#description` → `<CardGroup>` + `<Card>`\n  - `::block-hero` → the landing page\n  - `::code-block{label}` → a titled fence in `<CodeGroup>`\n  - `::video-player{src}` → `<video>`\n  - `::sandbox`, `::props`, `::copy-button`, `:ellipsis` → drop (report)\n- **URLs:** Nuxt Content 2 routes the same way, so check the old sitemap for trailing slashes and pin heading ids as above.\n\n## Dropped — report these\n\nReport each of these, and anything else you drop:\n\n- **Theme:**\n  - the brand color, when it was Docus's default emerald\n  - Nuxt UI overrides and theme CSS that doesn't map\n  - a custom `seo.titleTemplate`, `seo.schema`, and the TOC title\n  - logo extras, and socials with no Blume platform\n  - a forced color mode (the toggle stays)\n  - `navigation.sub: 'aside'` anchors\n  - a header button that showed only on the landing page (now site-wide)\n- **Navigation:** text-order sorting (`10.` before `2.`), and pages that sorted after a folder.\n- **Frontmatter:**\n  - `links` (now `related` cards)\n  - `sitemap: false`, `toc`, `layout`, and other unknown keys\n  - derived descriptions\n- **Components:**\n  - callout `to` links, callout colors (mapped to the nearest type; `<Callout color>` takes a CSS color when a tint matters), and card colors\n  - prompt icons and Windsurf/Claude actions\n  - the `code-tree` file browser and `code-preview` live rendering\n  - Nuxt UI landing decorations\n  - custom and module components\n  - brand icons with no SVG\n- **Server features:**\n  - the site's own MCP tools and prompts\n  - server routes and `@vercel/speed-insights`\n  - the assistant, if not kept\n  - old `/.well-known/skills/` file URLs, and pages whose prose describes any of these\n- **Anchors:** old ids that were already broken.\n\n<!-- File: .agents/skills/blume-migrate/references/docusaurus.md (Tokens: ~4040 | Category: anthropic-skill) -->\n# Docusaurus → Blume\n\nDocusaurus is a React site; navigation comes from `sidebars.js` (explicit or autogenerated) plus per-folder `_category_.json`. Admonitions are already directive syntax, which eases the content transform — **but note the source major version**: v2 (MDX v1) and v3 (MDX v3) have different content hazards, called out below.\n\n## Detect\n\n- **`docusaurus.config.{js,ts}`** at the root; a **`sidebars.{js,ts}`** file and per-folder **`_category_.json`** files.\n- Content under **`docs/`** (often plus `blog/`, `src/pages/`, `static/`, `versioned_docs/`); `@docusaurus/*` deps.\n- v2 vs v3: check the `@docusaurus/core` major in `package.json`.\n\n## Config: `docusaurus.config` → `blume.config.ts`\n\nRead `themeConfig`, `presets`, and `plugins`:\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` | `title` |\n| `tagline` | `description` |\n| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |\n| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |\n| `themeConfig.navbar.items` (external/utility links — Blog, Discord…) | **`navigation.actions`** (`[{ label, href }]`, plain header links) to keep them in the header, or **`navigation.featured`** (`{ label, href, icon? }`, pinned above the sidebar on every route) if they should survive on phones; the GitHub link → the `github` config instead (it renders in the footer) |\n| `themeConfig.navbar.items` (a `className`-styled button — \"Get started\", \"Sign up\") | **`navigation.cta`** (`{ label, href }`, the header's one filled button; a route the docs don't serve must be an absolute URL) |\n| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `\"system\"`) |\n| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |\n| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |\n| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md — GitHub Pages, a common Docusaurus host, is not one of them); `baseUrl` (when not `/`) → `deployment: { base: \"/…\" }`, or the `base` option of a host adapter (`vercel({ base })`) when the site also needs one |\n| preset `docs.routeBasePath` — **including the default!** | Docusaurus serves docs at **`/docs/…` by default**; the \"map only declared fields\" rule does **not** apply here because the _URLs_ are load-bearing. Either keep them with top-level **`basePath: \"/docs\"`** (invisible to the sidebar), or intentionally move to root and emit a `redirects` entry per page. Decide explicitly and say which. (`routeBasePath: '/'` = docs-only mode — nothing to do.) |\n| preset `docs.editUrl` | `github` (owner/repo/branch; a path after the branch → `github.dir`; **an origin other than `https://github.com` → `github.host`** — a GitHub Enterprise repo's edit links and header mark point at the public site without it) |\n| `themeConfig.footer` | `footer.links`, one row of `{ label, href }`: every column's items flatten into it in order (an item's `to`, a route, is its `href`); `copyright` (HTML) → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), keep a `${new Date().getFullYear()}` template literal as written, and drop a \"Built with Docusaurus.\" credit; a link inside it loses its URL (report, or add it to `footer.links`); a translated one in `i18n/<locale>/docusaurus-theme-classic/footer.json` → a locale map; column `title`s, `logo`, and `style` drop (report), or keep the whole footer as a Footer override (`defineComponents` layout slot) |\n| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |\n| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |\n| `themeConfig.algolia` | drop — Blume ships built-in search (Orama); remove the Algolia dep. To keep the index, `search: algolia({ appId, apiKey, indexName })` from `blume/search` (the DocSearch `apiKey` is the search-only key) |\n| `@docusaurus/plugin-client-redirects` | **static `redirects: [{from, to}]` arrays convert 1:1** to Blume `redirects` (a `from` array = one entry per item); only `createRedirects` functions are truly dynamic → host rules |\n| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |\n| OpenAPI doc plugins (`docusaurus-plugin-openapi-docs`, `redocusaurus`) | delete the plugin **and its generated pages** — add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list; `redocusaurus` `specs[].route` → the adapter's `route`, one `openapi()` entry (or source) per spec (see SKILL.md \"OpenAPI\") |\n| GraphQL doc generators (`@graphql-markdown/docusaurus`, `@edno/docusaurus2-graphql-doc-generator`) | delete the plugin **and its generated pages** — add `graphql({ spec, endpoint })` (from `blume/reference`) to the top-level `reference` list instead (see SKILL.md \"GraphQL\") |\n| `remark-math` + `rehype-katex` | delete — `$$…$$` renders in `.mdx` with no config (no `markdown.math` field exists), as a block or inline within a sentence; a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence |\n| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |\n\n## Static assets — required move\n\nEvery Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png` → `/img/foo.png`). Blume serves **`public/`**. **Move `static/*` (and any extra `staticDirectories`) into `public/`** — all root-relative references then keep resolving unchanged. Without this, every image and download 404s.\n\n## Navigation: `sidebars.js` + `_category_.json`\n\n- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.\n- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: \"group\"` (or `navigation.sidebar.display: \"group\"` once in config when every category collapses).\n- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder (its `title`/`description` from `link.title`/`link.description`) plus `directory: \"card\"` in the folder's `meta.ts`, which lists the folder's pages below it as the generated index did — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible: false` → that folder's `meta.ts` `display: \"flat\"`, so it stays a plain heading even under a global `\"group\"` mode. `className`/`customProps` → drop (report).\n- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.\n\n## Versioned docs\n\nDocusaurus `versioned_docs/version-X/` + `versions.json` (+ `versioned_sidebars/`) → **recommend migrating the latest released version only**. Mind the URL scheme: by default the **latest release** serves at `/docs/` and the work-in-progress `docs/` folder serves at `/docs/next` (`lastVersion: 'current'` flips this) — pick the folder that matches what users see at `/docs/`. If older versions must stay, use Blume's native versioning rather than a hand-built `navigation.selectors` dropdown: move each `versioned_docs/version-X/` into a top-level folder under `content.root` named for its id, and list that id in `versions.archived` (newest first), with `versions.current` labeling the live tree. Ids must start with a letter, so `version-1.0/` becomes `v1.0/`. Blume then adds the version switcher, the old-version notice, version-scoped search, and canonicals to the latest itself. A version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as ordinary current content. Snapshot routes become `/<id>/…`, so rewrite root-absolute links inside each snapshot to stay in it (`/guides/x` → `/v1.0/guides/x`) and add `redirects` from the old version URLs. Full reference: `docs/content/versioning.mdx` in the installed package.\n\n## Blog\n\nA Docusaurus `blog/` → Blume `type: blog` pages. **Dates come from filenames/folders** (`2024-01-31-foo.md`) — extract each into `date` frontmatter and strip the date from the filename (the old dated URLs `/blog/2024/01/31/foo` need `redirects`). Strip `<!-- truncate -->` / `{/* truncate */}` markers. `authors.yml` refs → inline author objects in each post's `authors` frontmatter. RSS stays at `/blog/rss.xml` on both sides. Blume generates **no** blog index, tag, author, or archive pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag, author, and archive pages as dropped.\n\n## Content & components\n\n- **`.md` vs `.mdx` — both majors need renames, for opposite reasons.** Blume parses `.md` as plain Markdown: no directives, no JSX, no `$$` math, no mermaid/package-install fences. **v3** treats `.md` as MDX (so a `.md` with imports/JSX/`{}` renders them as literal text in Blume); **v2** content is looser MDX v1. Rule: **rename any `.md` that contains admonitions, JSX, imports, or math to `.mdx`** — for typical Docusaurus repos that is most files.\n- **Admonitions are directives — but check the version.** v3: `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger` pass through; `:::caution` → `:::warning` (or rely on Blume's alias); titles `:::note[Title]` work. **v2:** titles are space-separated (`:::note Your Title`) — rewrite to brackets or the title is lost (`BLUME_DIRECTIVE_OPENING_TEXT` names each one); and v2's `:::warning` rendered **red/danger** — audit whether it should become `:::danger`.\n- **Tabs:** `<Tabs>`/`<TabItem label=\"…\" value=\"…\">` → `<Tabs>`/`<Tab title=\"…\">`. Drop `groupId`/`queryString`/`value`; strip the `@theme/Tabs` imports.\n- **Theme JSX in content:** `<DocCardList/>` (standard on category index pages) → delete it and set `directory: \"card\"` in that folder's `meta.ts`: Blume then lists the folder's pages as cards below the `index` page's content and keeps the list current (nested folders inherit the setting; give one `directory: \"none\"` to opt out). A hand-written `CardGroup` is only for a curated subset; `<TOCInline/>` → drop (report); `<CodeBlock>` JSX → a fenced code block; `<Admonition>` → the matching directive; `<details>`/`<summary>` → `<Accordion>`/`<AccordionItem>` or leave as raw HTML.\n- **`@theme/*` / `@site/*` imports** — strip `@theme/*` (Blume injects components globally); rewrite `@site/` asset/module paths to `/public` URLs or inline. **MDX partials** (`_partial.mdx` imports) → inline the partial's body (Blume's default `**/_*` exclude already hides the partial files themselves).\n- **Code blocks:** `title=\"file.js\"` → works as-is; `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left); **magic comments** (`// highlight-next-line`, `highlight-start`/`end`) → `{ranges}` or `// [!code highlight]` — unconverted they ship as literal comments in every sample; ` ```bash npm2yarn ` → ` ```package-install `.\n- **MDX v1 (v2 sources) pitfalls:** unescaped `<`/`{` in prose, HTML comments `<!-- -->` (→ `{/* */}`), string `style=\"…\"` attributes (→ objects). Fix as build errors surface.\n\n## Frontmatter\n\n| Docusaurus | Blume |\n| --- | --- |\n| `title` / `description` | pass through |\n| `id` | **not a safe drop** — Docusaurus builds a doc's URL from its ID, so `id: part1` in `guides/hello.md` serves `/docs/guides/part1`. Where the `id` differs from the filename (and no `slug` is set), replace it with `slug: guides/part1` (the folder path plus the id) or rename the file to match; otherwise drop it. Sidebar and `link.type: 'doc'` references name docs by this ID too |\n| `slug` | `slug` as a **full path from the content root**: Blume's `slug` replaces the page's whole route, while Docusaurus resolves a relative slug (no leading `/`) against the doc's folder. So `guides/intro.md` with `slug: start` → `slug: guides/start`; an absolute slug (`/start`) is already a full path |\n| `sidebar_label` | `sidebar.label` |\n| `sidebar_position` | `sidebar.order` |\n| `unlisted` | `hidden: true` + `noindex: true` |\n| `last_update.date` | `lastModified` |\n| `draft` | `draft` |\n| `tags` | `search.tags` (or drop if only used for the blog tag index) |\n| `image` | `seo.image` |\n| `keywords` | drop → use `seo` |\n| `sidebar_class_name` / `sidebar_custom_props` / `pagination_next` / `pagination_prev` / `pagination_label` / `hide_title` / `hide_table_of_contents` / `toc_min_heading_level` / `toc_max_heading_level` / `displayed_sidebar` / `custom_edit_url` | drop (report) |\n\nRemove any duplicated H1 in the body — `title` renders the H1.\n\n## Icons\n\nDocusaurus has no built-in icon-name system (icons are usually inline SVG/emoji/CSS). Where you add sidebar/tab icons, use Lucide names. Convert any FontAwesome/react-icons usage in content to Lucide `<Icon>` or drop it.\n\n## Teardown\n\nRemove `@docusaurus/*` and Algolia deps; delete `docusaurus.config.*`, `sidebars.*`, `babel.config.js`, and `src/` theme customizations (after the `src/pages/` inventory above; re-implement what matters via `defineComponents` layout slots or `blume eject`). Repoint `start`/`build`/`serve` scripts → `blume dev`/`blume build`/`blume preview`; add `blume`.\n\n## Dropped — report these\n\nCustom/swizzled theme components (layout slots or `blume eject`), the footer's logo and HTML items (and any link inside its copyright), the blog's generated tag, author, and archive pages, Algolia config, `sidebar_custom_props` and the other dropped frontmatter keys, `createRedirects` functions (→ host rules), React pages under `src/pages/`, `<TOCInline>`, per-category `className`/`customProps`, and any `@theme/*` component with no Blume equivalent.\n\n<!-- File: .agents/skills/blume-migrate/references/fern.md (Tokens: ~9580 | Category: anthropic-skill) -->\n# Fern Docs → Blume\n\nFern Docs is a hosted platform driven by a `fern/` folder: `docs.yml` declares the **entire** navigation (tabs, sections, pages, the API Reference, changelogs, versions, products), pages are MDX files anywhere under `fern/`, and the API Reference is generated from an OpenAPI/AsyncAPI spec or a **Fern Definition**. Two things set it apart from the other sources:\n\n- **The same `fern/` folder usually drives SDK generation too** (`generators.yml`, `definition/` or `apis/<name>/`). Only the docs move. Never delete or rewrite the API definition, a `generators.yml`, or `fern.config.json` (see [Teardown](#teardown-keep-sdk-generation)).\n- **URLs come from `docs.yml`, not from file paths.** A page's URL joins slugs from its tab, sections, and page name, and a frontmatter `slug` replaces most of that. Files sit wherever the team put them (`pages/get-started/welcome.mdx` can serve `/welcome`).\n\nScope: checked against Fern CLI 5.44.8 and an end-to-end migration of agentmail-to/agentmail-docs (115 pages, a Fern Definition with 149 endpoints and 8 webhooks, 270 old URLs). Versions, products, tab variants, translations, `folder:` entries, and OpenAPI (rather than Fern Definition) inputs weren't in that run: the guidance for them below is untested.\n\n## Detect\n\n- **`fern/docs.yml` beside `fern/fern.config.json`** (run from the repo root), or `docs.yml` + `fern.config.json` in the working directory (run from inside `fern/`). Monorepos nest it (`docs/fern/docs.yml`).\n- `fern.config.json` **without** a `docs.yml` is an SDK-only Fern project: the docs live elsewhere (often a Mintlify `docs.json`), so use that source's reference and leave `fern/` alone.\n- A stale `fern/docs.yml` can outlive a move off Fern. If the repo also carries another docs config, check which one the live site runs: Fern pages answer with an `x-fern-renderer` header (`curl -sI https://<docs-domain>/ | grep -i x-fern`).\n\n## Run the codemod first\n\nThe bundled codemod does the mechanical half: URL computation and file placement, `meta.ts`, frontmatter, the common components, assets, and every redirect. Steps, in order:\n\n1. **Save the old URL list.** Fetch `https://<docs-domain>/sitemap.xml` and keep only the paths, one per line, in `old-urls.txt` (a Fern sitemap can name a different host than the one you fetched). Hidden pages aren't in it; the codemod computes those from `docs.yml`. Run the CORS probe under [Try it](#layout-summary-snippets-try-it) while you're there.\n2. **Dry run**, from the folder that holds `fern/` (where `blume.config.ts` goes):\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt\n   ```\n\n   It prints a sitemap diff (`0 computed URL(s) not in it, 0 of its URLs not computed` is the goal; otherwise fix those pages by hand with [Fern's URL rules](#ferns-url-rules) and report them), the `navigation.tabs` entries for the config, which APIs are Fern Definitions, a TODO list of judgment calls, and the icons it couldn't map. `--out` sets the content root (default `docs`), `--public` the public folder, `--fern` the Fern folder. It **stops** on `versions` or `products`: migrate those by hand.\n\n3. **Apply** with `--write`. It moves every published page to a path that reproduces its old URL, writes frontmatter and `meta.ts` files, converts components, moves the assets pages use (and the logo and favicon) into `public/`, and saves `fern-migration.json`. A YAML or JSON file a page links to (a spec offered for download) is copied, not moved, since SDK generation may read it. It's idempotent: a rerun skips moved pages.\n4. **Write `blume.config.ts`** from [Config](#config-docsyml--blumeconfigts), with the printed tabs and `logo.href` set to the page `/` now redirects to.\n5. **Set up the API reference** ([API reference](#api-reference)): export a Fern Definition, write the overlay (tag names, webhooks, idempotency headers), add `openapi()`.\n6. **Build, then list the operation routes**:\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist > routes.json\n   npx fern-api@<version> export --api <api-name> spec.json   # JSON because the path ends in .json\n   ```\n\n   The join needs a JSON copy of the spec the build rendered: for a Fern Definition, that export (a scratch file, not the committed spec); for a YAML OpenAPI input, `npx @redocly/cli bundle <spec>.yaml --output spec.json`.\n\n7. **Join endpoints and write the redirects**:\n\n   ```bash\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt          # dry run\n   node <skill>/scripts/fern-codemod.mjs endpoints --routes routes.json --spec /api-reference=spec.json --sitemap old-urls.txt --write\n   ```\n\n   One `--spec <route>=<file>` per API. It maps every old endpoint, webhook, and group URL to its Blume route, translates `docs.yml` redirects, adds the content-move, changelog, feed, and `/` redirects, rewrites content links that point at old URLs, and checks every old URL is served or redirected (it must print `every old URL is served or redirected`). `--write` saves `fern-redirects.json`; import it:\n\n   ```ts\n   import fernRedirects from \"./fern-redirects.json\" with { type: \"json\" };\n   // in defineConfig:\n   redirects: [...fernRedirects],\n   ```\n\n   Fix a redirect it flagged by filtering that entry out and adding your own after the spread.\n\n8. **Work the TODO list**, then `blume build`, `blume validate --strict`, `blume audit --only redirects`, SKILL.md's heading-anchor check (`pin-heading-ids.mjs`), and `fern check` after the [teardown](#teardown-keep-sdk-generation).\n\nWhat it leaves to you: the config, the overlay, the TODO list, the components it names, links already broken on Fern, CI, and contributor docs. It never touches `generators.yml`, the definition, or a spec.\n\n## Inventory\n\nRead `docs.yml` first, then every file it points at: `redirects` (a list, or YAML files that each hold a `redirects:` list), `tabs.<key>.changelog`, every `path:` in `navigation`, `css`, `js`. **Every path in a Fern YAML file is relative to that YAML file.**\n\n- **Unpublished drafts.** Fern builds only the files `docs.yml` references plus each changelog folder; any other `.md`/`.mdx` under `fern/` is a draft nobody sees. The codemod leaves them in place and lists them. Blume would publish them: delete them or move them to a `_`-prefixed folder (`_drafts/`), and report the list.\n- **SDK generation.** Every `generators.yml` and its `groups` stay exactly as they are.\n- **Second instances.** Each `instances[]` entry is a deployment of the same docs (a second domain, a subpath). One Blume build serves one; report the rest (see [Deploy](#urls-redirects-and-deploy)).\n\n**Where the Blume project goes.** If the repo root has a `package.json`, put `blume.config.ts` there with content in `docs/` (the default root). If it has no JavaScript toolchain (a Python or Go repo whose docs were only `fern/`), make a self-contained docs package (`docs/` holding `package.json`, `blume.config.ts`, and `content.root: \"content\"`), run the codemod from there with `--fern ../fern --out content`, and say which you chose. Don't make `fern/` itself the content root: it keeps holding the SDK config. See `references/monorepo.md` for workspaces.\n\n## Config: `docs.yml` → `blume.config.ts`\n\nMap only what's set.\n\n| Fern `docs.yml` | Blume | Notes |\n| --- | --- | --- |\n| `title` | `title` | Fern shows it only in the browser tab; Blume also shows it beside the logo (see `logo`) |\n| `logo` (`{ light, dark, href, height, right-text }`) | `logo: { image: { light, dark, alt }, href, text }` | the codemod moves the files into `public/`. Fern shows the image alone unless `right-text` is set, so set **`text: right-text`, or `text: \"\"`**. A local monochrome SVG can collapse to `logo: \"/logo.svg\"` with `currentColor` fills (SKILL.md). Set `href` to the first page when nothing serves `/`. `height` drops |\n| `favicon` | **drop the field** | the codemod moves the file to `public/favicon.<ext>`; Blume detects it by filename |\n| `colors.accent-primary` (or `accentPrimary`), a string or `{ light, dark }` | `theme.accent` | same shape |\n| `colors.background` | `theme.background` | same shape |\n| `colors.*`, `layout.*`, `settings.*` (others), `theme.*`, `global-theme`, `page-actions`, `experimental`, `check`, `ai-examples`, `libraries`, `roles` | **drop** (report) | `global-theme` merged a registry theme at publish: read its values off the live site |\n| `typography.bodyFont` / `headingsFont` / `codeFont` | `theme.fonts.body` / `display` / `mono` | Fern fonts are files in the repo (`path`, or `paths` with `weight`/`style`): each role takes `{ name, variants: [{ src, weight, style }] }` |\n| `navbar-links` `type: filled` / `primary` | `navigation.cta` (`{ label: text, href }`) | one only; extra filled links go to `actions`. `url` is Fern's old name for `href` |\n| `navbar-links` `type: minimal` / `outlined` / `secondary` | `navigation.actions` (`[{ label: text, href }]`) | text only: link icons drop. A `mailto:` link works |\n| `navbar-links` `type: github` / `dropdown` | `github: { owner, repo }` / **no equivalent** | flatten a dropdown into `actions` (report) |\n| `footer-links` | `footer.socials` | keys carry over except `twitter` → `x`, `hackernews` → `hacker-news` |\n| `footer` / `header` (React files) | **drop** (report) | or a layout slot in `components.ts` |\n| `instances[].custom-domain` | `deployment.site` (`https://…`) | only when the target host isn't Vercel or Netlify (SKILL.md). **A domain with a path** (`acme.com/docs`) is a reverse-proxied subpath → `deployment: { base: \"/docs\" }`. **Several instances** → one Blume build; keep the canonical one (`metadata.canonical-host` names it when set) and report the rest |\n| `instances[].edit-this-page.github` (`{ owner, repo, branch }`) | `github: { owner, repo, branch, dir }` | `dir` = path from the repo root to the folder holding `blume.config.ts` |\n| `metadata.canonical-host` | `deployment.site` | same host rule as `custom-domain`; a path in it is the `base` above |\n| `metadata.og:description` / `twitter:site` | `description` / `seo.x.handle` | other `og:*`/`twitter:*` drop: Blume writes them |\n| `announcement.message` | `banner` | a string, or `{ content, link: { text, href } }` |\n| `analytics.*` | the matching factory from `blume/analytics` (`posthog`, `googleAnalytics`, `googleTagManager`, `segment`), or `script()` | a `${VAR}` value → `process.env.VAR` |\n| `js` (a path, `{ path, strategy }`, or `{ url, strategy }`) | `script({ src \\| content, strategy })` per tracking script | a third-party loader ports; code that reads or restyles Fern's DOM (`.fern-*`) doesn't (report it). Fern's strategies (`beforeInteractive`, `afterInteractive`, `lazyOnload`) aren't Blume's (`async`, `defer`): pick one or leave it out. A `<link rel=\"help\" href=\"/llms.txt\">` injection drops: Blume publishes `llms.txt` |\n| `css` | a root `theme.css` | `.fern-*` selectors match nothing in Blume; report what you drop |\n| `default-language`, `settings.http-snippets` | `codeSamples` on `openapi()` | that language first; a list → that list; `false` → `false` |\n| `settings.disable-search` | `search: false` |  |\n| `layout.hide-feedback` | `feedback: false` |  |\n| `ai-search` / `ai-chat` (Ask Fern) | **drop** (report) | Blume's assistant is opt-in and self-hosted (`ai.assistant`, a provider key, server output); offer it, don't enable it silently |\n| `agents.*` | **drop**, or `agents.llmsTxt.details` for directive text | Blume generates `llms.txt` and `robots.txt` |\n\n## Navigation\n\n### Fern's URL rules\n\nThe codemod applies these; you need them for its sitemap diff and anything it doesn't handle. Each level contributes a slug, joined outermost first: **product → version → tab → sections → page**.\n\n- **Each slug is the item's `slug:` if set, else lodash `kebabCase` of its display name**: tab `display-name` (not the tab's key), `section:` title, `page:` name. `kebabCase` lowercases, drops punctuation, and splits camelCase and letter/digit boundaries: `API Reference` → `api-reference`, `AgentMail` → `agent-mail`, `SOC 2` → `soc-2`, `v3 (Latest)` → `v-3-latest`.\n- **`skip-slug: true`** on a tab, section, or api entry drops that level from the URL but keeps it in the sidebar. A changelog tab ignores it.\n- **A frontmatter `slug` is the page's whole URL below any product/version prefix**: it replaces the tab and section slugs too. A `docs.yml` `slug:` replaces only that item's own segment.\n- **An `api` entry's base** is its `summary` page's frontmatter `slug` when set; otherwise its `slug` (or `kebabCase(title)`) under the tab.\n- **A changelog** serves its index at its slug and **each entry at `<changelog>/<YYYY>/<M>/<D>`** (no zero padding), titled with the date (`September 5, 2026`). Two entry files dated the same day share one URL: Fern merged them into one page.\n- **`/`** answers with a 307 to the first page when no landing page is set. **API group paths** (`/api-reference/inboxes/threads`) answer with a 307 to the group's first endpoint and aren't in the sitemap, but content links to them.\n- **The first version is the default and has no version segment**; every other version is prefixed with its slug. Products always carry their slug.\n\n### What the codemod builds\n\n- **One folder per tab** at the tab's URL prefix. The tab whose pages share no single prefix (a `skip-slug` tab, or one whose pages all carry root-level frontmatter slugs) becomes the content root, with a tab at `path: \"/\"`. Only one tab can be the root; the codemod reports a second one, which must move under a prefix (route change, redirects).\n- **A section** whose pages share a URL segment → a real folder; one that adds no segment → a `(group)` folder. A section's `path` (its overview page) → that folder's `index.mdx`.\n- **A page whose URL departs from its folder** (a frontmatter `slug`) stays in its sidebar folder with `slug` set, since Blume places a page in the sidebar by file path, not route.\n- **A file `docs.yml` lists twice** is published once, at its first listing; the other URL redirects there (reported).\n- **`meta.ts`** per folder: `pages` in `docs.yml` order, the section title and Lucide icon, and a collapsible section (`collapsed`, `collapsible`, `collapsed-by-default`) → `display: \"group\"`. Blume lists loose pages above subgroups, so a section that interleaves pages and subsections renders its pages first: report any such reorder.\n- **Labels.** Fern labels a page in the sidebar with its `page:` name (or `sidebar-title`) and titles it with frontmatter `title`. So: no `title` → `title: <page name>` (or an H1 that opens the body, which it removes); a different `title` → `sidebar.label: <page name>`. The api **summary** page keeps its `title` with no label (its page name usually repeats the tab label).\n- **`hidden: true`** → `hidden: true`, `seo: { noindex: true }`, and `search: { exclude: true }`: Fern keeps hidden pages reachable but out of search and indexing.\n- **The root URL.** Nothing serves `/` under a root tab, so it redirects `/` to the first page (307), as Fern did; set `logo.href` to that page. The tab at `/` links to the sidebar's first page on its own. A root `index.mdx` would move the landing page to `/`; do that only if the user wants `/` as the landing URL.\n\nBy hand: `availability` (`beta`, `deprecated`, …) → `sidebar.badge` (`Beta`) or `deprecated: true`; `- link:` items → `navigation.featured` (`{ label, href, icon? }`) or an explicit `navigation.sidebar` link (report which); a hidden **tab** (its printed line ends `(hidden on Fern)`) → leave it out of `navigation.tabs`; the codemod already marked its pages hidden.\n\n### Tabs\n\nThe codemod prints one `navigation.tabs` entry per tab (`{ label: display-name, path, icon }`). Keep them as header tabs.\n\n- An **`href` tab** (Blog, GitHub, a dashboard) has no content → `navigation.actions` or `navigation.featured`; a GitHub tab → `github`.\n- **Tab `variants`** (several sidebars under one tab): the codemod reports them. One subfolder per variant and a tab with `items` (`{ label, path }` per variant); variant pages move, so add redirects and report the approximation.\n\n### Changelog\n\nThe codemod moves each entry to `changelog/<YYYY-MM-DD>[-suffix].mdx` (`MM-DD-YYYY` and `MM-DD-YY` names are renamed, so entries sort by date and a two-digit year's month isn't read as an ordering prefix. Fern parsed those in the publishing machine's time zone, so one can sit a day off: the sitemap diff shows it) with `type: changelog`, `date`, and a `title`: the frontmatter one, the date as Fern showed it, or the file suffix sentence-cased (`agentid-sign-in-keys` → \"Agentid sign in keys\"). **Review every suffix title** (`AgentID sign-in keys`); the TODO list names them. `tags` → `changelog.category` (the first) and `search.tags` (all).\n\n- **Same-day entries.** The day's Fern URL redirects to the unsuffixed entry, or else the first by file name. Fern showed both on one page: consider linking the second from the first, and report it.\n- `overview.mdx` (or `index.mdx`, `summary.mdx`) → its text goes into `changelog: { title, description }` in `blume.config.ts`, since an `index.mdx` would replace Blume's generated `/changelog` index (a list, where Fern's index rendered whole entries). Its `layout` and `authors` drop.\n- A non-date file (`TEMPLATE.mdx`) was never published: move it out or `_`-prefix it, and update it to Blume's format if contributors copy it.\n- The feeds `/<changelog>.rss` (`.atom`, `.json`) redirect to `/changelog/rss.xml`, which needs a site URL.\n- For a public GitHub repo, offer `githubReleases()` as SKILL.md \"Changelogs\" says; keep the files when the user says the entries are curated rather than release notes.\n\n### Versions and products (by hand, untested)\n\n- **`versions`** → Blume's native versioning (`docs/content/versioning.mdx` in the installed package). The first Fern version is the live tree at the content root; every other version → a folder named by its slug, listed in `versions.archived`. Place each version file's `navigation` with the same rules; a `ref:` version lives in git, so check it out. Per-version API references → one `openapi()` each, with its own `route`.\n- **`products`** → one folder per product plus `navigation.selectors: [{ kind: \"product\", label, items }]`.\n- `<If versions products>` → keep each folder's matching branch.\n\n## Content and components\n\nThe codemod writes every page as `.mdx` and converts:\n\n- **Callouts.** `<Note>`, `<Tip>`, `<Info>`, `<Warning>`, `<Success>`/`<Check>`, `<Error>`, `<Launch>`, and `<Callout intent>` (no intent → `info`, as Fern rendered it). A top-level one becomes a `:::type[Title]` directive (dedented). One whose icon maps to Lucide (a directive can't set one), or one nested in a component, becomes `<Callout type title icon>`; an icon with no Lucide equivalent drops (reported).\n- **Code groups.** `<CodeBlocks>` → `<CodeGroup>`; every untitled fence inside a group gets Fern's language title (`title=\"Python\"`, `title=\"cURL\"`). Blume labels an untitled grouped fence by its language anyway, so the title only keeps Fern's spelling. `<CodeBlock title>` wrappers are unwrapped onto the fence: Blume's `<CodeBlock>` renders a wrapped fence, but not the wrapper's `title`.\n- **Fence metas.** Multi-word bare titles are quoted (Fern and Blume both read every word of ` ```js Snippet with title` as the title), `filename=` → `title=` and `wordWrap` → `wrap` (Blume warns `BLUME_CODE_FENCE_OPTION` on the Fern spellings), `env` → `dotenv` (Shiki has no `env`: `BLUME_UNKNOWN_CODE_LANGUAGE`).\n- **Cards and accordions.** `<Cards>` → `<CardGroup>`, card icons → Lucide, Fern-only card props drop. `<AccordionGroup>`/`<Accordion>` → `<Accordion>`/`<AccordionItem>`; a lone `<Accordion>` is wrapped in one.\n- **`<Icon>`**: icon → Lucide, `size` × 4 (Fern counts in 4px units). **`<Frame>`**: `background` drops. **`<Tab language>`**: `language` drops (Blume syncs same-titled tabs).\n- **`<ParamField path>`** → `<ParamField name>`: Fern's `path` is the field's name, and Blume's `path=` would mark it a path parameter. `toc` drops. Nested fields in an `<Indent>` (reported) → an `<Expandable title=\"properties\">` in the parent field's body.\n- **Snippets.** `<Markdown src=\"/snippets/x.mdx\" planName=\"x\" />` → `<include planName=\"x\">/_snippets/x.mdx</include>` on a line of its own, with the snippet moved to `<content-root>/_snippets/`. Its `{{planName}}` placeholders carry over as written; Blume reads an include prop only when its name starts with a lowercase letter, so rename any that don't.\n- **Embeds and comments.** A YouTube iframe → `<YouTube id>` (a playlist embed → `<YouTube url>`); `allowfullscreen`/`frameborder` → `allowFullScreen`/`frameBorder` (MDX drops the lowercase forms silently); `<!-- -->` → `{/* */}`.\n- **Assets.** `/assets/x.png` and `../x.png` → moved into `public/` at their path from `fern/` and linked root-absolute. A YAML or JSON file is copied instead (SDK generation may read it; the copy goes stale when it changes). An asset no page or config references stays put: move or delete it.\n- **Links.** Absolute links to the docs' own domain → root-relative; links to moved pages, endpoints, and Fern's `api:METHOD/path` syntax → the new routes (the `endpoints` step).\n\n**Reported for you to convert** (a leftover Fern tag gets a `BLUME_UNKNOWN_COMPONENT` warning, or crashes the page):\n\n| Fern | Blume |\n| --- | --- |\n| `<EndpointRequestSnippet endpoint=\"POST /x\">`, `<EndpointResponseSnippet>` | a hand-written fenced sample plus a link to the operation page: Blume can't embed an operation's sample in prose |\n| `<EndpointSchemaSnippet>`, `<Schema>`, `<SchemaSnippet>`, `<WebhookPayloadSnippet>`, `<RunnableEndpoint>` | a link to the operation page; a small schema can become a `TypeTable` |\n| `<Badge intent outlined>`, `<Availability type>` | `<Badge variant stroke>` (`success`, `warning`, `error` → `danger`, `tip` → `accent`); an availability that labels the page → `sidebar.badge` |\n| `<Code src title>` | `<include meta='title=\"…\"' lang=\"…\">./path</include>` with the file inside the content root |\n| `<Files>`/`<Folder>`/`<File>` | `<Tree>`/`<Tree.Folder>`/`<Tree.File>` |\n| `<Versions>`/`<Version title>` | `<Tabs>`/`<Tab title>` |\n| `<If>` (roles), `<Feature flag>` | the public or default content; ask before dropping role-gated text |\n| `<Button href>`, `<Download src>`, `<Copy>` | a link (or `<Card href>`), a link to the file in `public/`, inline code |\n| `<Aside>`, `<Indent>`, `<StickyTable>`/`<SearchableTable>`/`<PaginatedTable>` | the content inside |\n| `<Prompt title>`, `<ScrollWalkthrough>` | `<Prompt description>`; `<Steps>` with a code block per step |\n| `<ChangelogTags>`, `<Template>` | delete (report) |\n\nNot reported, but check: headings inside `<Steps>` (Fern makes each a step) → one `<Step title>` each; `<Anchor id>` → `<a id=\"x\"></a>`; `<llms-only>`/`<llms-ignore>` → `<Visibility for=\"agents\">`/`<Visibility for=\"web\">`; `maxLines` → `expandable`; inline `$…$` math → `$$…$$`; custom React components → an island or a `components.ts` override. **Unclosed fences** (Fern rendered an empty block): the codemod closes them at the end of the file and reports them; delete the stray fence.\n\n### Frontmatter\n\nFern ignores unknown keys; Blume's schema is strict. The codemod maps `title`, `sidebar-title` → `sidebar.label`, `subtitle` → `description` (Blume renders `description` under the title), `description` → `seo.description` when `subtitle` took `description`, `headline` → `seo.title`, `slug`, `noindex` → `seo.noindex`, and the `docs.yml` `icon`. It reports every other key; map these by hand:\n\n| Fern | Blume |\n| --- | --- |\n| `canonical-url` | `seo.canonical` (absolute) |\n| `image`, `og:image` | `seo.image` |\n| `keywords` | `search.keywords` (a list) |\n| `last-updated` | drop it everywhere and set `lastModified: \"git\"` site-wide; retire any workflow that stamps it, since the key now fails the build |\n| `layout` | `mode`: `overview`/`reference` → `wide`, `page` → `center`, `custom` → `custom`, `guide` → drop |\n| `hide-nav-links: true` | `pagination: false` |\n| `availability` | `sidebar.badge`, or `deprecated: true` |\n| `position` (orders pages in a `- folder:` entry) | `sidebar.order` |\n| `hide-toc`, `force-toc`, `max-toc-depth`, `hide-feedback`, `hide-page-actions`, `no-image-zoom`, `breadcrumb`, `search-metadata`, `edit-this-page-url`, `logo`, `nofollow`, `jsonld:breadcrumb`, `og:*`, `twitter:*`, `excerpt`, `tags` | drop (report) |\n| keys Fern never read (`sidebar_position`, `lastUpdated`) | drop |\n\n### Icons\n\nFern uses Font Awesome; Blume is Lucide-only. The codemod normalizes Fern's spellings (`fa-solid fa-bolt`, `solid bolt`, bare `bolt`), maps them to Lucide names Blume bundles, and reports the rest. Most brand icons (`discord`, `google`, `aws`, `python`, `node-js`, `x-twitter`) have no Lucide equivalent: they drop. An image path on a component (`icon=\"./icons/x.svg\"`) → `public/` and a root-absolute path; one in `docs.yml` is reported for you to move. For icons you write yourself (config, hand conversions), check each name from the folder holding `blume.config.ts`; an unknown icon renders nothing:\n\n```bash\nnode -e \"const p=require.resolve('@iconify-json/lucide/icons.json',{paths:[require('path').dirname(require.resolve('blume/package.json'))]});const j=require(p);for(const n of process.argv.slice(1))console.log(n,n in j.icons||n in (j.aliases||{}))\" zap mail-check discord\n```\n\n## API reference\n\nEach `- api:` entry → one `openapi()` (or `asyncapi()`/`graphql()`) source from `blume/reference`, with **`route` set to the api entry's old base URL** so the overview keeps its address, and a tab at that route. Find the spec each entry renders:\n\n- `specs:` on the api entry → those files (`type: openapi | asyncapi | graphql`).\n- Otherwise `api-name: <name>` → `fern/apis/<name>/`, or `fern/` with one API. There, `generators.yml` `api.specs` lists OpenAPI/AsyncAPI files (older projects use top-level `openapi:`/`async-api:`); **a `definition/` folder is a Fern Definition**, which the codemod's dry run names.\n\n### OpenAPI and AsyncAPI inputs\n\n- **Point `spec` at the file where it lives** (SDK generation reads it too).\n- **Fern `overrides`** are Fern's deep-merge format, not OpenAPI Overlays. Most hold only `x-fern-*` SDK extensions, which Blume ignores; content an override changes for readers → an Overlay in `overlays`. Fern's own `overlays:` files carry over.\n- **`x-fern-ignore: true` operations would appear** in Blume: remove them with an overlay action (`target: $.paths.*[?@['x-fern-ignore'] == true]`, `remove: true`), likewise operations an api entry's `audiences` filtered out.\n- gRPC and OpenRPC references have no Blume renderer: report them.\n\n### Fern Definition inputs → export to OpenAPI\n\nBlume can't read a Fern Definition. Export it with the Fern CLI, which runs offline with no login:\n\n```bash\nnpx fern-api@<version from fern/fern.config.json> export --api <api-name> openapi/<api-name>.yml\n```\n\n- The output is **OpenAPI 3.0.1** (Blume upgrades it). Operation ids are `<package path>_<endpoint>` (`inboxes_threads_list`); each Fern package becomes one PascalCase tag (`InboxesThreads`); `info.title` is the API's name with an empty `version`.\n- **It drops webhooks, WebSocket channels, and `idempotency-headers`.** Restore the first and last with the overlay below; a WebSocket channel has no Blume page, so redirect its old URL to the guide that covers it and report it.\n- **A committed export.** If the repo already commits an OpenAPI generated from the definition (a `fernapi/fern-openapi` generator group writing locally, or a CI step running `fern export`), point `spec` at it, then check it isn't stale: export to a scratch path and diff. If they differ, report it and refresh it with the repo's own command (`fern generate --group <group> --local`), not a second copy. Look for workflows that trigger on that file (a release pipeline) before committing a refresh. Only when nothing produces a committed file, commit your export and add a `\"docs:spec\"` script that reruns it.\n\nDocs-only changes go in an overlay so the spec stays regenerable. The agentmail overlay, abridged:\n\n```yaml openapi/docs.overlay.yaml\noverlay: 1.1.0\ninfo: { title: Docs fixes, version: 1.0.0 }\nactions:\n  - target: $.info\n    update: { title: AgentMail API }\n  # Tags are flat in Blume: name the concatenated package tags.\n  - target: $.paths.*.*.tags[?@ == 'InboxesThreads']\n    update: Inbox Threads\n  # Fern's sidebar order.\n  - target: $\n    update:\n      tags: [{ name: Inboxes }, { name: Inbox Threads }]\n  # Webhooks the export dropped: their payload schemas survive in components.\n  - target: $\n    update:\n      x-webhooks:\n        messageReceived:\n          post:\n            summary: Message Received\n            tags: [Webhook Events]\n            requestBody:\n              content:\n                application/json:\n                  schema: { $ref: \"#/components/schemas/MessageReceivedEvent\" }\n            responses:\n              \"200\":\n                description: Return a 2xx status to acknowledge the event.\n  # One action per `idempotent: true` endpoint in the definition.\n  - target: $.paths['/v0/inboxes/{inbox_id}/messages/send'].post.parameters\n    update:\n      - name: Idempotency-Key\n        in: header\n        schema: { type: string }\n```\n\n**Restore webhooks this way** rather than redirecting their pages: one `x-webhooks` entry per webhook in the definition, keyed by its name, with a `$ref` to its payload schema. Blume upgrades `x-webhooks` to `webhooks` and gives each a page at `<route>/<tag-slug>/<kebab-name>`, which the `endpoints` step matches to the old URL. Nested packages (`Inboxes › Threads`) become sibling tags: report the lost nesting.\n\n### Layout, summary, snippets, Try it\n\n- **`summary`** (a page shown at the API's root) → the codemod places it at `<route>/introduction.mdx`, a normal page in the reference sidebar. The old base URL serves Blume's overview, which renders `info.description`.\n- **`layout`**, `alphabetized`, `flattened`, `paginated` → Blume orders tag groups by the top-level `tags` list (set it in the overlay) and operations within a tag in the spec's order (paths as listed, each path's methods as written). An endpoint order that `layout` or `alphabetized` set and the spec doesn't share → a `pages` list of operation slugs in `<route>/<tag-slug>/meta.ts`, or report it. An endpoint's `title` → its `summary` via overlay; `hidden: true` endpoints → remove them or let them show (ask).\n- **`snippets`** (SDK package names) → Fern rendered SDK calls; Blume generates HTTP-client samples. Add `x-codeSamples` through the overlay to keep SDK calls, or report the loss.\n- **Try it.** Fern's API Explorer proxies requests; Blume's playground sends them from the browser, so the API must answer CORS for the docs origin. Probe it:\n\n  ```bash\n  curl -si -X OPTIONS https://<api-host>/<path> -H 'Origin: https://<docs-domain>' -H 'Access-Control-Request-Method: GET'\n  ```\n\n  No `Access-Control-Allow-Origin` in the answer → `playground: false`, and report it; the alternative is `playground: { proxy: true }`, which needs server output and a host adapter (SKILL.md). `environments: []` → `playground: false`; `oauth: true` → Blume takes a pasted token and doesn't run the flow (report).\n\n### API URLs\n\nThe `endpoints` step computes these; check its output against them.\n\n- **Fern Definition**: the operation id split on `_`, each part kebab-cased, under the base: `inboxes_threads_list` → `/api-reference/inboxes/threads/list`.\n- **OpenAPI**: with `x-fern-sdk-method-name`, `<base>/<x-fern-sdk-group-name>/<x-fern-sdk-method-name>`; otherwise `<base>/<first tag>/<the operation id minus the tag's leading words>`, or the summary when there's no operation id. Each part kebab-cased (`v1` → `v-1`). Read from Fern's parser, not from a migrated site: the `endpoints` step's coverage check catches any URL it gets wrong.\n- **Group paths** → the overview's anchor for that tag: `/api-reference/inboxes/threads` → `/api-reference#inbox-threads` (307, as Fern's was). Content linking to a group gets the same anchor; `blume validate` checks it.\n\nBlume routes rarely match Fern's (the `endpoints` step skips any that do), and a pattern redirect over the old prefix also covers the new pages (`BLUME_REDIRECT_MATCHES_PAGE`), hence one exact redirect per endpoint. Don't shorten operation ids with an overlay: OpenAPI needs them unique across the whole spec, and shortened ones repeat (`list` in every tag), which warns `BLUME_OPENAPI_DUPLICATE_OPERATION_ID`.\n\n## URLs, redirects, and deploy\n\n- **`docs.yml` redirects** → `{ from, to, status }`: Fern's `permanent` defaults to true (308), `permanent: false` → 307; Blume's own default is 301, so the codemod writes the status. `:name` and `:name*` carry over; `:name+` and regex groups are reported (expand them into exact entries). A destination that moved is followed to its final route, and a `:slug*` pattern into the reference expands into one entry per operation. Trailing-slash duplicates merge.\n- **A destination that's neither a page nor another redirect** was already broken on Fern (the TODO list names it): point it at the intended page and report it.\n- **`blume validate --strict` finds links that already 404 on Fern.** Fix the link, don't add a redirect for it, and report the list.\n- **A subpath instance** (`acme.com/docs`): sources, destinations, and links carry the subpath; strip it, since Blume adds `base` itself. Serving both a domain and a subpath means two builds.\n\n## Teardown (keep SDK generation)\n\n**Keep**, even with no docs left in it: `fern/fern.config.json`, every `generators.yml`, the Fern Definition, every spec, override, and overlay a `generators.yml` references, and CI steps that run `fern check`, `fern generate --group <sdk>`, or `fern export` (with the `FERN_TOKEN` secret if they use it).\n\n**Remove**, once migrated: `fern/docs.yml`, redirect YAML files, the docs folders that moved (pages, `changelog/`, `snippets/`, assets, docs `components/`, `css`/`js`), CI steps and wrapper scripts that publish or preview the docs (`fern generate --docs`), and the `fern-api` dependency only if nothing else uses the CLI.\n\nThen **grep the repo for the old paths and syntax**: `fern/pages`, `fern/changelog`, `fern/snippets`, `docs.yml`, `<CodeBlocks>`, `<Note>`, `last-updated`. Contributor docs (`AGENTS.md`, `CONTRIBUTING.md`, templates) and generators that write into `fern/snippets/` need the new paths, frontmatter, and components. Run `fern check` afterwards.\n\nRepoint `package.json` scripts to `blume dev`/`build`/`preview`, add `blume`, and regenerate the lockfile (SKILL.md step 6). Tell the user what can't be committed: moving the custom domain's DNS (or the proxy behind a subpath instance) off Fern, then unpublishing the site in the Fern dashboard.\n\n## Dropped: report these\n\n- **Hosted features**: the API Explorer's proxy, login-injected credentials, and OAuth flow; Ask Fern search and chat; AI-generated examples; **Fern's docs MCP server (`/_mcp/server`)**, which has no static equivalent (Blume's MCP server is at `/mcp` and needs server output; `.md` page mirrors and `llms.txt` carry over), so update content that advertises it; the dashboard, analytics, preview links, RBAC and authenticated docs (host-level protection instead, per the deployment docs' **Private docs** section), feature flags.\n- **API reference**: SDK snippets (unless rebuilt as `x-codeSamples`), WebSocket channel pages, a `layout` or `alphabetized` endpoint order you didn't rebuild with `meta.ts`, nested sections, hidden endpoints, gRPC/OpenRPC references.\n- **Navigation and theme**: dropdown and link icons, approximated variants, sidebar reorders, moved `link` items, extra `colors`, `layout`, custom `header`/`footer`, `css` aimed at `.fern-*`, and `js` that isn't a tracking script.\n- **Content**: API embeds replaced by samples or links, code tooltips, `<ChangelogTags>`, role gating, and each dropped frontmatter key.\n- **Icons** with no Lucide equivalent, by name and where they were used.\n- **Unpublished drafts** and what was done with them.\n- **Second instances** and what replaces them.\n\n<!-- File: .agents/skills/blume-migrate/references/fumadocs.md (Tokens: ~4146 | Category: anthropic-skill) -->\n# Fumadocs → Blume\n\nFumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present.\n\n## Detect\n\n- Content under **`content/docs/`** (the Fumadocs convention).\n- Per-folder **`meta.json`** files.\n- `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps, and collections declared either in a `source.config.ts` (the Config API) or, in newer scaffolds, with `defineDocs` from `fumadocs-mdx/macro` inside `lib/source.ts` (no `source.config.ts` at all).\n- A `loader({ baseUrl })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`). Newer scaffolds pass a constant (`baseUrl: docsRoute`) defined in `lib/shared.ts` — resolve it there.\n\n## Config\n\nFumadocs declares little Blume needs — read the loader and the shared layout options:\n\n- **Title** — the header title: `nav.title` in `baseOptions` (`lib/layout.shared.tsx`, or `app/layout.config.tsx` in older scaffolds; often the `appName` constant from `lib/shared.ts`; extract the text if it's JSX). Without one, read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case); for a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead.\n- **Header settings** (the same `baseOptions`): `githubUrl` → `github: { owner, repo }` (the repository and edit links; when it's built from a `gitConfig` constant in `lib/shared.ts`, `{ user, repo, branch }` → `{ owner: user, repo, branch }`); `links` entries → `navigation.actions` (`{ label: text, href: url }`), except a `type: \"icon\"` link to a platform `footer.socials` knows (Discord, X, …), which goes there instead. Menus, custom items, and JSX in `nav` → report.\n- **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: \"/docs\"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `\"/\"`, serve from the site root (nothing to set).\n\nEverything else is Blume defaults.\n\n**Also read the MDX options** — remark/rehype plugins live in `source.config.ts` `mdxOptions`, or, with the macro API, in the collection options passed to `defineDocs` (a macro project may still keep a `source.config.ts` for global options):\n\n- `remark-math`/`rehype-katex` → nothing to configure: `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field), as a block or inline within a sentence. A single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence.\n- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.\n- Custom Shiki transformers/themes or other plugins → report.\n\n## Navigation: `meta.json` → `meta.ts`\n\n**Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`):\n\n| Fumadocs `meta.json` | Blume `meta.ts` |\n| --- | --- |\n| `title` | `title` |\n| `icon` | `icon` — **convert the casing** (see Icons below) |\n| `defaultOpen: false` | `collapsed: true` |\n| `defaultOpen: true` | `collapsed: false` |\n| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |\n| `pages: [...]` slugs | `pages: [...]` (ordering) |\n| `description` | **drop** (folders have no description) |\n| `collapsible: false` | that folder's `display: \"flat\"` (a plain heading, even under a global `\"group\"` mode) |\n\n`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`, `directory` (`\"card\"`/`\"accordion\"` lists the folder's pages below its `index` page — the replacement for a hand-written card list of the folder's children). Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: \"group\"` (drill-in is `\"page\"`); when the whole sidebar should collapse, set `navigation.sidebar.display: \"group\"` once in `blume.config.ts` instead of repeating it per folder.\n\n**Numeric filename prefixes move URLs.** Fumadocs builds slugs from the file path as written, so `01-intro.mdx` serves `/docs/01-intro`; Blume reads a `01-` prefix on a file or folder as sort order and drops it from the route (`/docs/intro`). Add a `redirects` entry for every prefixed file or folder (or pin the old path with frontmatter `slug`, a full path from the content root), and write `pages` entries without the prefix (`\"intro\"`, not `\"01-intro\"`).\n\n**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.\n\nHandle the `pages` array items:\n\n- **`\"...\"`** (rest marker) / `\"\"` → drop; Blume appends unlisted pages automatically. **`\"z...a\"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.\n- **A `pages` list with no `\"...\"`** → Fumadocs shows **only** the listed items; every page and folder it leaves out is hidden from the sidebar (still served by URL). Blume appends unlisted children, so each one **resurfaces** — set `sidebar.hidden: true` in each left-out page's frontmatter (for a left-out folder, in every page under it — a folder with no visible page drops out of the sidebar).\n- **`\"!page\"`** (exclusion from `\"...\"`) → the same trap: Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead.\n- **`\"---Section---\"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead.\n- **`\"...folder\"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.\n- **`\"[Text](url)\"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`).\n- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.\n\n## Frontmatter\n\nFumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). **`full: true`** (full width, TOC folded into a popover) → `mode: wide` (full column width, no TOC — the closest match; report); **`_openapi`** marks a generated API stub — delete the whole page (see OpenAPI). Any other non-schema key is a build error, so drop and report.\n\n## Icons: lucide-react names → kebab-case\n\nFumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`\"BookOpen\"`, `\"HomeIcon\"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons).\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare `<Callout>`** → `:::info`. `title` → `:::type[Title]`. An `icon` (a JSX element there) needs a `<Callout type=\"…\" title=\"…\" icon=\"cpu\">` with its kebab-case Lucide name, since a directive can't set one; drop it if it only restated the type's icon.\n- **Cards:** `<Cards>` → `<CardGroup>`. `<Card>` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={<Cpu />}` imported from `lucide-react`) → Blume takes a string name (`icon=\"cpu\"`); Fumadocs `description=\"…\"` → Blume has no `description` prop — move the text into the card body.\n- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).\n- **File trees:** `<Files>` → **`<Tree>`** (the JSX container for `Tree.*` children — _not_ `<FileTree>`, which wraps a Markdown list); `<Folder>` → `<Tree.Folder>` (`defaultOpen` carries over); `<File>` → `<Tree.File>`. Or convert the whole block to a list-driven `<FileTree>`.\n- **Tabs:** Fumadocs declares labels on the parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value=\"npm\">`. Blume's `<Tab>` carries its own `title`. Strip `items={[…]}` from `<Tabs>` and give each child `<Tab>` a `title` (from its `value`, or the positional `items` entry).\n- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.\n- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link: { href, text }, dismissible, id }`).\n- **`<include>./partial.mdx</include>`** — Blume uses the same include syntax. **Pass through unchanged**: paths resolve relative to the including file, a leading `/` resolves from the content root, and nested includes work. Keep partials in `_`-prefixed files or folders so they stay out of routing and the sidebar.\n- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.\n- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory the MDX components file before deleting it** — `components/mdx.tsx` (`getMDXComponents`) in newer scaffolds, `mdx-components.tsx` in older ones; components registered there are used import-free in MDX bodies; port or inline each usage first.\n\n## Headings\n\nTrailing heading markers — `[#custom-id]` (pinned anchor), `[!toc]` (hide from the TOC), `[toc]` (TOC-only entry) — use the same syntax in Blume. **Pass through unchanged.** One exception: Fumadocs' looser grammar accepts an id containing whitespace (`[#two words]`); Blume does not parse that as a marker, so rewrite such an id to a hyphenated one and update every link that targets it.\n\n## Code fences\n\n- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.\n- Adjacent fences sharing `tab=\"…\"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.\n\n## OpenAPI\n\n`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, add `openapi({ spec })` (from `blume/reference`) to the top-level `reference` list (vendor the spec locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.\n\n## GraphQL\n\n`@fumadocs/graphql` works differently from the OpenAPI flow: **no stub files** — pages are virtual, served through the Loader API. The artifacts to harvest and tear down: `lib/graphql.ts` (`createGraphQL()` — read its `input` for the schema and any per-source routes/labels), the `graphql.staticSource()`/`graphql.loaderPlugin()` wiring in the source config, the `GraphQLPage` component (`createGraphQLPage()`, usually `components/api-page.tsx` — read its playground `endpoint`), and the `@fumadocs/graphql/css/preset.css` import. Map to a `graphql({ spec, endpoint })` entry in the top-level `reference` list (see SKILL.md \"GraphQL\"): SDL files/text and introspection results carry over as `spec` (vendor a URL input locally); a programmatic `GraphQLSchema` instance must be printed to SDL and committed (report); the `createGraphQLPage` playground endpoint becomes `endpoint`. When `createGraphQL()` takes several inputs, or sources carry their own routes, labels, or endpoints, map each one to an entry in the adapter's `sources` (`{ spec, route, label, endpoint }`) instead of the single `spec` shorthand — otherwise a schema is dropped or mounted on the wrong route. Blume groups routes the same way (`<route>/queries/<field>`, `<route>/objects/<type>`), but slugs are re-derived — rewrite inbound links and let `blume validate` catch strays. Then remove `@fumadocs/graphql`, the two lib/component files, and the CSS import. Since there are no generated pages, there is nothing to delete from the content tree — but keep any hand-written conceptual pages under the reference route.\n\n## i18n\n\nA `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's default `dir` parser as-is; locale **file suffixes** (`page.cn.mdx` beside an unsuffixed default-locale `page.mdx`) match the `dot` parser as-is — add `parser: \"dot\"`, no file moves. Under `dot` a folder's `meta.ts` serves every locale, so build it from the default locale's `meta.json` and report any translated folder titles in `meta.<lang>.json`. Report whichever transform you apply.\n\n## Package.json & teardown\n\nRepoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*`/`@fumadocs/*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `lib/source.ts`, `lib/shared.ts`, `lib/layout.shared.tsx` (or `app/layout.config.tsx`), `components/mdx.tsx` / `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).\n\n## Dropped — report these\n\nFolder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full` (approximated as `mode: wide`); header settings from `lib/layout.shared.tsx` with no Blume home; `<DynamicCodeBlock>`/`<InlineTOC>`; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.\n\n<!-- File: .agents/skills/blume-migrate/references/gitbook.md (Tokens: ~8616 | Category: anthropic-skill) -->\n# GitBook → Blume\n\nGitBook is a hosted editor. This reference covers the Markdown it writes when a space syncs to GitHub or GitLab (Git Sync): `SUMMARY.md` (a space's navigation), `.gitbook.yaml` (where its files live, plus redirects), and, on site-wide Git Sync, `gitbook-docs.yaml` (which space maps to which directory). Theme, logo, header and footer links, custom domain, site redirects, and access control live in the GitBook app, not the repo. Two things set this migration apart:\n\n- **URLs follow `SUMMARY.md`, not the file tree.** Moving a page in the editor changes its URL but not its file. Blume derives route _and_ sidebar from the file path, so the core move is putting each file at its old URL.\n- **Blocks are Liquid-style tags (`{% hint %}`) and raw HTML inside `.md`.** Almost every page becomes `.mdx`, and MDX rejects several things GitBook writes.\n\n## Detect\n\n- **`.gitbook.yaml`** or **`.gitbook.yml`** (GitBook reads both) at the root or in a space directory; **`gitbook-docs.yaml`** for site-wide Git Sync.\n- A **`SUMMARY.md`** (GitBook writes it opening `# Table of contents`; a hand-written one may open `# Summary`), with `* [Title](path.md)` items and `## Group` headings, beside a **`.gitbook/`** folder (`assets/`, `includes/`, maybe `vars.yaml`). `.gitbook.yaml` is optional, so this pair can be the only sign.\n- Pages that open with a `# Title` line and use `{% hint %}`, `{% tabs %}`, `{% content-ref %}`; sync commits titled `GITBOOK-<n>: …`.\n- **Not this format:** `book.toml` + `src/SUMMARY.md` is mdBook. `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit — see [Legacy GitBook CLI](#legacy-gitbook-cli-bookjson).\n\n## First, collect what the repo doesn't hold\n\nDo this while the GitBook site is still published. Nothing moves before step 1.\n\n1. **Old URLs.** `<site>/sitemap.xml` indexes one `sitemap-pages.xml` per published space (`/sitemap-pages.xml`, `/de/sitemap-pages.xml`, …); fetch them all. They list every canonical page, hidden and generated OpenAPI pages included, but not `noIndex`/`noRobotsIndex` pages (take those from `SUMMARY.md`). `<site>/llms.txt` lists the same pages with their `.md` URLs. Save the paths, one per line, outside the content root.\n2. **Site redirects** (Settings → Redirects). The app has no export: `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/redirects?limit=1000` with a token (paginated: while the response has `next.page`, fetch again with `&page=<it>`), or ask the user. With neither, report it as a follow-up.\n3. **Site customization.** `GET https://api.gitbook.com/v1/orgs/{organizationId}/sites/{siteId}/customization` with a token. Without one, read the published HTML: logo and favicon URLs go through `~gitbook/image?url=<original>` (decode `url` and download it), the header carries a light logo (`class=\"block dark:hidden …\"`) and, when one is set, a dark one (`hidden dark:block`), and the inline CSS declares the primary color as an RGB triplet, `--primary-original`.\n4. **Spaces not in the repo.** Compare the sitemap index with the repo's directories. A space with `content.directory: null`, or a language or version the site serves but the repo lacks (often GitBook-made translations), has no Markdown here. Ask: export it (Git Sync that space into a branch, or `POST https://api.gitbook.com/v1/spaces/{spaceId}/git/export`) and migrate it; regenerate it with `blume translate` once `i18n` is set; or drop it and redirect its URLs (see URLs and redirects). Report the choice.\n\n## Config\n\nMap what step 3 found or the user supplies; leave the rest at Blume's defaults.\n\n| GitBook | Blume |\n| --- | --- |\n| Site title (`gitbook-docs.yaml` `site.title`) | `title` |\n| Logo (light and dark) | `logo: { image: { light, dark } }`, files in `public/` (SKILL.md covers monochrome SVGs and wordmarks) |\n| Icon (favicon) | `public/icon.svg` or `icon.png` (a dark one → `icon-dark.*`); no config field. An emoji icon has no file: make one or report it |\n| Primary color | `theme.accent` |\n| Default mode, fonts, code theme | `theme.mode`, `theme.fonts.{body,display,mono}`, `markdown.code.theme` (nearest Shiki themes) |\n| Theme style, tint and semantic colors, corner, depth, link, and sidebar styles | drop (report); a root `theme.css` if one matters |\n| Header links; a button-styled one | `navigation.actions`; `navigation.cta` |\n| Announcement | `banner` (`{ content, link, dismissible, id }`) |\n| Footer | `footer.links`, flattened into one row; copyright → `footer.copyright`; group titles and logo drop (report) |\n| Social accounts | `footer.socials` |\n| Social preview image | `seo.image` on the landing page (Blume renders a card for every other page) |\n| Git Sync repository and branch | `github: { owner, repo, branch }`, plus `dir` or `host` when needed; GitLab has no equivalent (report) |\n| Page actions: Markdown and AI providers, MCP, PDF | defaults; `agents.mcp.enabled` plus server output (follow-up); `export` |\n| GitBook Assistant | `ai.assistant` plus server output (follow-up), or drop |\n| Integrations | adapters in `analytics` from `blume/analytics`; others `script()` or report |\n| Custom domain | the DNS record moves to the new host; keep the URL as `deployment.site` unless the host is Vercel or Netlify |\n| Custom subdirectory (`example.com/docs`) | `deployment: { base: \"/docs\" }` |\n| Share links, authenticated access | host-level protection (the deployment docs' **Private docs**); report |\n| `.gitbook/vars.yaml` | `variables` (see Variables) |\n\n**Content root.** `root: ./docs/` is Blume's default `content.root`. With `root: ./`, `git mv` the space (pages, `SUMMARY.md`, `.gitbook/`) into `docs/` — routes are relative to the content root, so they don't change — or scope `content.include` (`references/monorepo.md` §1). Left unscoped, the repo's `README.md` and every stray `.md` publish.\n\n## Navigation: `SUMMARY.md` → folders\n\n### How GitBook builds a URL\n\n- The first `SUMMARY.md` item (`structure.readme`, `README.md` by default) is served at the space root; its own slug (`/readme`) redirects there.\n- Every other page: the space's prefix (see Site structure), then its `## Group`'s slug, each ancestor page's slug, and its own. A page's slug comes from its file name at import (a `README.md` → its folder's name), lowercased; a group's, from its heading (emoji dropped) or the `id` of an `<a id=\"…\">` inside it.\n- **The file's own path doesn't count.** A page filed at `configuration/post-install.md` but listed under group \"Deployment\" → parent `deployment/permissions/README.md` publishes at `/deployment/permissions/post-install`.\n\nCompute every page's URL from `SUMMARY.md`, then check them against the sitemap list; the sitemap wins.\n\n### Put every file at its old URL\n\nOne `git mv` per page keeps both the route and the sidebar:\n\n- A page with no children → `<url>.mdx`; with children → `<url>/index.mdx`; the landing page → `index.mdx` at the content root (`.md` for a page that stays plain). Lowercase every segment: Blume keeps a file name's case, so `OCR.md` would publish at `/OCR` where GitBook served `/ocr`.\n- **`README.md` isn't an index in Blume** (`guides/README.md` publishes at `/guides/README`): every README becomes `index`.\n- Each `## Group` becomes a top-level folder named for its slug.\n- Keep a table of old file → new file → route: links, includes, and image paths are rewritten from it.\n- A file not in `SUMMARY.md` was never published. Delete it, or prefix it with `_` to keep it unpublished, and report it.\n- Delete `SUMMARY.md` once the folders and `meta.ts` files carry its structure; it would publish as a page.\n\n### Order, labels, and display → `meta.ts`\n\n- **Order.** Each folder's `meta.ts` lists its children in `SUMMARY.md` order (`pages`); the content root's lists the loose pages, then the group folders.\n- **Labels.** A page's title is its `SUMMARY.md` link text, and a link title (`[Title](page.md \"Label\")`) → `sidebar.label`. A group folder's `meta.ts` `title` is the heading text, and **a parent page's folder gets `title: <its SUMMARY text>` too** — otherwise the row shows the humanized folder name (`deployment-guides` → \"Deployment Guides\" for a page titled \"Getting started\").\n- **Display.** GitBook shows groups as plain headings and each parent page as one expandable row. Set `navigation: { sidebar: { display: \"group\" } }`, `display: \"flat\"` in each group folder's `meta.ts`, and `sidebar: { hidden: true }` on each parent's `index`: the folder row already links to it, so without this the parent lists twice.\n- **Contents pages.** A parent whose body is empty or only content-refs to its own children → delete those and set `directory: \"card\"` in the folder's `meta.ts`. It inherits: set `directory: \"none\"` on a nested folder that isn't a contents page.\n- **Order Blume can't match.** Loose top-level pages always list above groups and folders. A parent page among loose pages (Overview, Editions ▸, Use Cases) drops below them, and pages after a `***` divider in `SUMMARY.md` (top-level again: `/uninstall`, not `/other/uninstall`) rise above the groups. Move such pages into a parenthesized group folder (`(more)/`, which adds no URL segment) ordered in the root `meta.ts`, or accept the order; report either.\n- **External links** (`* [Website](https://…)`) → `navigation.featured` or `navigation.actions`; report the move.\n- **Hidden pages.** GitBook's `hidden: true` hides a page's whole subtree; Blume's hides one page, so set it on the index and every descendant (a folder whose pages are all hidden leaves the sidebar).\n- A list item that is a YAML block with `type: builtin:openapi` is a generated API reference (see OpenAPI).\n\n### Site structure: `gitbook-docs.yaml`\n\n`site.structure` nests `section-group`, `section`, `space`, and `external-link` entries. Every entry has a `key` (meaningless to Blume), a `title`, and maybe `localizedTitle`; most can carry `draft` or `condition`. Sections and spaces have a `path` and maybe `default: true` (a section group has no `path`); section groups, sections, and external links can have an `icon`; a space can be `hidden` and has `content.directory` (`null` when not synced) and maybe `content.language`. Skip `draft` entries, and treat a `condition` like adaptive content (report it).\n\n- **URL prefixes.** The default section's default space is at the site root. Another space → `/<section>/<space>`; a non-default section's default space → `/<section>`. A site without sections serves its default space at the root and the rest at `/<space>`.\n- **Sections** → a top-level folder each plus a `navigation.tabs` entry (`icon` → Lucide). The default section stays at the content root; add `{ label, path: \"/\" }` to show it as a tab.\n- **Section groups** → a dropdown tab scopes the sidebar by one `path`, so give each section its own tab (the grouping is lost; report) or move them under one prefix (URLs change; pattern redirects).\n- **Spaces within a section:** languages → `i18n` (Blume puts the locale first, site-wide: `/payments/fr/x` → `/fr/payments/x`); versions → `versions` (top-level `/<id>/…`, ids start with a letter); products → a folder each and a `navigation.selectors` dropdown. Redirect the old prefixes.\n- A space's directory can sit inside another's (`./home/fr` in `./home`): separate them, then do the `SUMMARY.md` work per space. A `hidden: true` space is still published but left out of the space switcher: keep its pages, leave it out of `navigation.selectors`, and report it. `localizedTitle` → a per-locale tab label.\n- **External links** (`external-link`, at the root or in a section group) → `navigation.actions` entries (`{ label, href }`; `localizedTitle` → a per-locale `label`, the `icon` drops).\n\n## Content and components\n\n### `.md` or `.mdx`\n\nRename to `.mdx` every page with a `{% … %}` block, `$$` math, a mermaid fence, or a component after conversion. Directives, components, math, mermaid, `package-install`, and `ts2js` are MDX-only: in `.md`, a leftover `{% hint %}` or `:::note` is literal text with a green build (each `{% … %}` tag warns `BLUME_TEMPLATE_TAG` at its line, a `:::note` `BLUME_MD_DIRECTIVE`), and in `.mdx` a leftover `{% … %}` fails it (`BLUME_MDX_SYNTAX`, which `blume validate` and `blume check` report too). Plain pages can stay `.md`; tables, footnotes, fence meta, `<include>`, `{{variables}}`, `[#id]`, and raw HTML work in both. An include is spliced as text, so it's parsed in the including page's format.\n\n### MDX hazards\n\nEach of these fails `blume build` in `.mdx` (`blume check` and `validate` name the ones MDX can't parse as `BLUME_MDX_SYNTAX`, at their line):\n\n- **Unclosed `<img …>` and `<br>`** (`BLUME_MDX_UNCLOSED_ELEMENT` names each). Close them — better, convert images to Markdown.\n- **HTML comments** → `{/* */}`.\n- **A `{` outside code.** GitBook's `\\{{x\\}}` isn't enough; write `` `{{x}}` `` or `\\{\\{x\\}\\}`. Braces inside raw HTML (`<code>CN={{DeviceId}}</code>`, `<td>${EMAIL}</td>`) compile, then fail the render: `blume build` names each as `BLUME_MDX_UNDEFINED_NAME` at its line. Turn `<code>`, `<strong>`, `<a>`, `<p>`, and `<br>` inside table cells into Markdown.\n- **Markdown inside JSX** (`<Tab>`, `<Step>`, `<Frame>`) needs blank lines around it.\n\nGitBook's `\\<` and entities (`&#x20;`, `&#x3C;`) are fine (an escaped `\\<Name` can still warn `BLUME_UNKNOWN_COMPONENT`; the page renders correctly). Strip the U+200B/U+200C characters GitBook leaves at the start of some code lines.\n\n### Blocks\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` (the first line) | frontmatter `title`; delete the line |\n| `{% hint style=\"info\" %}` (`success`, `warning`, `danger`) | `:::info` (`:::success`, `:::warning`, `:::danger`); legacy plugin `tip` → `:::tip`, `working` → `:::note`. A directive takes no `icon`: keep one that matters as `<Callout type=\"<type>\" icon=\"<lucide>\">`, else drop it; inside another directive, give the outer one a longer fence (`::::`) |\n| `{% tabs %}` + `{% tab title=\"X\" icon=\"y\" %}` | `<Tabs>` + `<Tab title=\"X\" icon=\"<lucide>\">`; `fullWidth` drops. Blume switches same-titled tabs together page-wide (`sync={false}` opts a group out) |\n| `{% code title=\"a.js\" lineNumbers=\"true\" overflow=\"wrap\" expandable=\"true\" %}` | fence meta: ` ```js title=\"a.js\" lineNumbers wrap expandable `; `fullWidth` drops. **A fence with no language takes `text`** (` ```text lineNumbers `), or the first option becomes the language (`BLUME_UNKNOWN_CODE_LANGUAGE`) |\n| `<pre class=\"language-x\" data-title=\"…\" data-line-numbers data-overflow=\"wrap\"><code>` | a fence built from those attributes (others drop), entities decoded, `<strong>`/`<em>` stripped |\n| `{% stepper %}` + `{% step %}` | `<Steps>` + `<Step>`. **A step that opens with a heading keeps it as the first line inside `<Step>`**: GitBook publishes it as an anchored heading in the outline, and `title` renders a plain paragraph with no anchor. A bold-line opener → `title` |\n| `{% columns %}` + `{% column %}` | `<Columns cols={n}>` + `<Column>`; widths drop |\n| `<details><summary>T</summary>` (`open`) | `<Expandable title=\"T\">` (`defaultOpen`); a run → `<Accordion>` + `<AccordionItem title=\"T\">`. A title renders inline Markdown, so write a summary's `<code>`, `<strong>`, and `<a>` as Markdown in it: raw HTML there shows as text |\n| `{% content-ref url=\"x.md\" %}`; legacy `{% page-ref page=\"x.md\" %}` | `<Card title=\"<x's title>\" href=\"<x's route>\" />` (the link text is a file name, so look the title up); a run → `<CardGroup>`; links to a parent's own children → `directory: \"card\"` |\n| `[text](x.md#h \"mention\")` | a plain link without `\"mention\"`, its text replaced by the target's title |\n| `{% embed url=\"…\" %}`, often with no `{% endembed %}` | YouTube → `<YouTube url=\"…\" />` (`?t=122` → `start={122}`; `url` ignores `t`); anything else → `<Card title=\"…\" href=\"…\" />`. A caption → `<Frame caption=\"…\">` around it (YouTube's `title` only reaches screen readers) |\n| `{% file src=\"…\" %}caption{% endfile %}` | `[caption](../assets/x.pdf)`, relative to the page: Blume publishes a file a relative link names with the page. For a URL that never changes, put the file in `public/files/` and link `/files/x.pdf` |\n| `<figure><img src alt><figcaption>` | `![alt](relative/path.png)` (optimized); a caption → `<Frame caption=\"…\">` around it; `<div data-with-frame=\"true\">` → `<Frame>`. A raw `<img>` with a relative `src` works too, but unoptimized |\n| `<img class=\"gitbook-drawing\">` | a Markdown image |\n| inline `<img data-size=\"line\">` | a Markdown image (renders at its own size), or `<img src=\"/<public path>\" alt=\"\" height=\"20\" />` to stay text-height (report) |\n| `<picture>` with a dark-mode source | the light image (report) |\n| `<table data-view=\"cards\">` | `<CardGroup cols={3}>` (`data-card-size=\"large\"` → `cols={2}`), a `<Card>` per row: first text cell → `title`, other text → body, the `data-card-target` link → `href`, the `data-card-cover` file → `img`, relative to the page like a Markdown image (`img=\"../assets/cover.png\"`), an `<i class=\"fa-…\">` cell → `icon` |\n| other `<table>` (`data-header-hidden`, `data-search`, cells with `<p>`/`<br>`) | a GFM table (a hidden header's first row becomes the header); a cell's `<ul><li>` can stay inline in `.mdx` |\n| `$$…$$`, a ` ```mermaid ` fence | unchanged, in `.mdx` |\n| Emoji: `:rocket:`, escaped `:ballot\\_box\\_with\\_check:`, `<span data-gb-custom-inline data-tag=\"emoji\">☑️</span>` | the character itself: Blume renders no shortcodes |\n| `<i class=\"fa-x\">:x:</i>` | `<Icon icon=\"<lucide>\" />`, or delete it |\n| `<a class=\"button primary\">`, `<button data-action=\"…\">` | a link or a `<Card>` with `cta`; search and ask buttons drop |\n| `<mark style=\"color:…\">` | `<mark>` or bold; report colors that carried meaning |\n| `## Heading <a href=\"#x\" id=\"x\"></a>` | works as written: the heading takes `x` as its id. `## Heading [#x]` is the tidier spelling |\n| `[^1]` annotations | unchanged: footnotes at the page's end, not hover popups |\n| `{% updates %}` + `{% update date=\"…\" %}` | `type: changelog` pages (SKILL.md \"Changelogs\"), or dated `##` sections |\n| `{% prompt description=\"…\" %}` | `<Prompt description=\"…\">` |\n| `{% if visitor… %}` | keep what the public saw, drop the tags (report) |\n\n### Links\n\n- Every moved file breaks the relative links into and out of it. Rewrite internal links from your table as root-absolute routes (`/deployment/permissions#roles`): Markdown links, content-ref URLs, card `href`s, and `href`s in HTML. A `README.md` target → its folder's route; absolute links to the old domain → routes.\n- Cross-space links `https://app.gitbook.com/s/<spaceId>/<path>` (or `…/o/<orgId>/s/…`) → the target space's prefix plus `<path>`; `GET https://api.gitbook.com/v1/spaces/{spaceId}` gives its `urls.published`. Links into `…/~/changes/…` or to `/broken/pages/<id>` were already broken: fix or report them.\n\n### Images and assets\n\n- Move `.gitbook/assets/` to `assets/` at the space root and replace the string `.gitbook/assets/` with `assets/`, then recompute the paths in every moved page.\n- File names have spaces and parentheses. `![](<../assets/image (1).png>)` and `../assets/image%20(1).png` both work, in a page and in an `_includes/` partial alike: a partial's images are rebased onto the including page in any form.\n- GitBook keeps every upload, so many files in `assets/` may be referenced by nothing. Report them and offer to prune.\n\n### Variables and expressions\n\n- `.gitbook/vars.yaml` → `variables`; `<code class=\"expression\">space.vars.NAME</code>` → `{{NAME}}`.\n- `page.vars.NAME` (from `vars:` frontmatter) has no page scope: write the value inline. Read inside an include, it becomes a prop: `<include name=\"Computer\">/_includes/x.md</include>` with `{{name}}` in the partial (prop names are lowercase).\n- An expression with logic (`a ? \"x\" : \"y\"`) → evaluate it and write the result.\n- Once `variables` is set, an undefined `{{name}}` in prose fails the build: placeholders that are content (`{{DeviceId}}`) go in inline code.\n\n### Includes (reusable content)\n\n- `.gitbook/includes/x.md` → `_includes/x.md` at the content root, and `{% include \"../../.gitbook/includes/x.md\" %}` → `<include>/_includes/x.md</include>` on its own line (a leading `/` resolves from the content root).\n- **Demote every heading in an include one level:** GitBook renders an include's `#` at a page's `##`.\n- Delete includes nothing references. One referenced by ID (`{% include \"/reusable-content/…\" %}`) isn't in the repo: copy it from the app.\n\n### OpenAPI\n\nGitBook has five forms: a `SUMMARY.md` YAML item `type: builtin:openapi` naming an organization spec (`dependencies.spec.ref.spec: <slug>`); `{% openapi src=\"…\" path method %}`; `{% openapi-operation spec=\"<slug>\" path method %}` and `{% openapi-schemas %}`; the older `{% swagger src=\"…\" path method %}`; and hand-written `{% swagger method path baseUrl %}` blocks with `swagger-parameter` and `swagger-response` (oldest: `{% api-method %}`).\n\n- **Specs.** Vendor each one into the repo — a `<slug>`'s file comes from the app's OpenAPI panel or `GET https://api.gitbook.com/v1/orgs/{organizationId}/openapi/{specSlug}/versions/latest/content/raw` — and add `openapi({ spec })` from `blume/reference` with a `navigation.tabs` entry (SKILL.md \"OpenAPI\").\n- **`builtin:openapi`** → set `route` to the old reference's path and keep its parent page's prose under it. Operation URLs change, so redirect each old one in the sitemap (SKILL.md \"OpenAPI\" covers generating them).\n- **Inline operations** → a link or `<Card>` to the generated page. `{% openapi-schemas %}` has no counterpart (report).\n- **Hand-written blocks** → a `<ParamField query|path|header|body=\"…\">` per parameter (a `cookie` has no slot), the responses as titled fences in `<ResponseExample>`, and `api: \"GET /path\"` frontmatter when it's the page's only endpoint (`baseUrl` → `api.server`).\n- `x-codeSamples` carries over; GitBook's other `x-*` extensions are ignored (report).\n\n## Frontmatter\n\n| GitBook | Blume |\n| --- | --- |\n| `# Title` / `SUMMARY.md` link text | `title`; where they differ, GitBook published the `SUMMARY.md` text |\n| `SUMMARY.md` link title | `sidebar.label` |\n| `description` (often folded `>-`) | `description` (quote a plain value containing `: `) |\n| `icon` (Font Awesome) | `icon`, in Lucide (see Icons) |\n| `hidden: true` | `hidden: true`, on the page and every descendant |\n| `noIndex: true` | `search.exclude: true` + `noindex: true`, on the page and every descendant |\n| `noRobotsIndex: true` | `noindex: true`, on the page and every descendant |\n| `cover`, `coverY`, `coverHeight`, `tags`, `if` | drop (report); a cover worth keeping → an image at the top of the body |\n| `vars` | inline the values, then drop |\n| keys left by older tooling (`category`, `order`) | drop: GitBook ignored them |\n\n`layout`:\n\n| GitBook `layout` | Blume |\n| --- | --- |\n| every part visible, `width: default` | delete the block |\n| `tableOfContents.visible: false` | `mode: center`; with `title.visible: false` too, `mode: custom` |\n| `outline.visible: false`, or `width: wide` | `mode: wide` |\n| `title.visible: false`, sidebar shown | `mode: frame` |\n| `pagination.visible: false` | `pagination: false` |\n| other parts hidden; a preset (`layout: landing`) | drop (report); for a preset, match the published page |\n\nA Blume mode hides more than one part: `center` and `wide` also hide the outline, and `frame` hides it and the page-end links and footer. Report it where GitBook kept those.\n\nBlume's frontmatter is strict: remove every key the tables drop.\n\n## Icons\n\nGitBook uses Font Awesome names in frontmatter `icon`, `<i class=\"fa-…\">`, tab, hint, and button icons, and section icons. Blume is Lucide-only.\n\n- **Frontmatter:** `node <skill>/scripts/mintlify-codemod.mjs <content-dir>`, then `--write`. Its icon pass is the same Font Awesome → Lucide map, and GitBook frontmatter has none of the keys its other rules touch.\n- **Everything else by hand,** with the table in `references/mintlify.md`. Common GitBook names: `circle-info` → `info`, `clock-rotate-left` → `history`, `sliders` → `sliders-horizontal`, `handshake-angle` → `handshake`, `angles-up` → `chevrons-up`, `puzzle-piece` → `puzzle`, `life-ring` → `life-buoy`, `shield-halved` → `shield`, `file-contract` → `file-text`.\n- **Check every name** against the set Blume bundles; an unknown name renders nothing:\n\n  ```bash\n  node -e 'const b=require.resolve(\"blume/package.json\");const s=require(require.resolve(\"@iconify-json/lucide/icons.json\",{paths:[b]}));for(const n of process.argv.slice(1))console.log(n,!!(s.icons[n]||s.aliases?.[n]))' zap history sliders-horizontal\n  ```\n\n## URLs and redirects\n\nWith every file at its old URL, pages need no redirects. The rest are below. GitBook answers all of them with a 307, except a permanent site redirect (308). Blume's default `status` is 301: keep it for a settled move (`.gitbook.yaml` redirects, recovered automatic redirects), and set `status: 307` where the target may change (group paths, dropped translations).\n\n- **`.gitbook.yaml` `redirects`** (`old/path: new/file.md`, relative to the space) → `{ from: \"/<space prefix>/old/path\", to: \"<new/file.md's route>\" }`.\n- **Site redirects** (`source`, `destination`, `permanent`, `captureWildcard`, `draft`) → `{ from: source, to, status: permanent ? 308 : 307 }`. `destination` is an object, not a URL: `kind: \"external\"` carries `url`; `site-page` (`siteSpaceId`, `pageId`), `site-space`, and `site-section` name ids. Resolve those to the old URL — `GET …/sites/{siteId}/site-spaces` gives each site space's `urls.published` and `space.id`, `…/sites/{siteId}/sections` each section's `urls.published`, and `GET /v1/spaces/{spaceId}/content` each page's `path` within its space — then to the Blume route at that URL. A trailing `*` with `captureWildcard` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`; without it, every match goes to the one `to`. Skip drafts (the list leaves them out by default), and write `from` without the site base.\n- **Automatic redirects.** GitBook quietly 307s every former URL of a moved or renamed page, and lists them nowhere. Recover them: run every historical `SUMMARY.md` (`git log --format=%h -- SUMMARY.md`, then `git show <sha>:SUMMARY.md`) through the URL rule above, add the route each file's path implies, and request each candidate from the live site (`curl -s -o /dev/null -w '%{http_code} %{redirect_url}'`). Each 307 to a page you kept becomes a redirect.\n- **Group paths and the landing slug.** GitBook redirects a group's own path to its first page, and `/readme` to `/`. Add both (`status: 307` for the group paths).\n- **Dropped translations.** GitBook translates group slugs (`/de/zahlungen/…` for group \"Payments\"), so `/de/:path*` → `/:path*` sends most translated URLs to 404s. Diff each language's `sitemap-pages.xml` against the default one, add a pattern per translated group (`{ from: \"/de/zahlungen/:path*\", to: \"/payments/:path*\", status: 307 }`), then each language's catch-all last: patterns are tried in order.\n- **Case.** GitBook matched URLs case-insensitively; Blume doesn't. Lowercased files cover the canonical URLs; report mixed-case inbound links.\n- **Markdown URLs.** GitBook serves `<url>.md`, and so does Blume; redirects move the `.md` URL with its page.\n- Rewrite internal links **before** adding redirects: `blume validate` accepts a redirect's `from` as a link target.\n\n### Heading anchors\n\nGitBook makes heading ids its own way: colons, slashes, and a spaced dash become one hyphen each, dots stay, an id that would start with a digit gets `id-`, and ids stop at 100 characters (`AppConfig:OCSP:UseAuthorizedResponder` → `appconfig-ocsp-useauthorizedresponder`, where Blume writes `appconfigocspuseauthorizedresponder`). Every `#anchor` link GitBook wrote to such a heading fails `blume validate`, and links from other sites land at the top of the page. A rule predicted only 890 of 934 ids on a real site, so read them instead. After `blume build`, from the project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com          # report\nnode <skill>/scripts/pin-heading-ids.mjs --old https://docs.example.com --write  # pin\n```\n\nIt fetches each old page once (one request at a time, cached in `.pin-heading-ids-cache/`; delete it after), pairs its headings with the build's by text, and appends the old id to each differing source heading — `[#id]` in `.mdx`, `{#id}` in `.md`, `\\{#id\\}` in a partial. Rebuild and rerun until it reports 0. A `NOTE … no source line` is a heading the source doesn't spell out (a component renders it, or it was reworded): pin it by hand. `--only-linked` pins only ids that content links to. A page whose URL changed (a space under a new prefix) is looked up on the old site through `--map`, a JSON list of exact `{ from, to }` entries (patterns are skipped): `dist/blume-redirects.json` works as it is, but a build writes it only when no host adapter is set. Any `BLUME_BROKEN_ANCHOR` left after that was already dead on GitBook: retarget the obvious ones, drop the fragment from the rest, and report them.\n\n### Final check\n\nRequest each old URL from `blume dev` or `blume preview` (both apply pattern redirects) with `curl -sL`, or check it against `dist/` and `dist/blume-redirects.json` (written only when no host adapter is set), then run `blume audit --only redirects` after the build.\n\n## Legacy GitBook CLI (`book.json`)\n\nThe old `gitbook-cli` toolchain and its fork HonKit:\n\n- **`book.json`:** `root`, `structure`, `title`, `description`, `variables`, `plugins`. Map `title` and `description`; `root` → `content.root`; plugins → whatever Blume has built in, else report.\n- **URLs are file paths:** `a/b.md` → `/a/b.html`, a `README.md` → its folder's `index.html`. Keep the files where they are, rename READMEs to `index`, and add an exact redirect from each `.html` URL (a pattern can't strip the extension).\n- **`SUMMARY.md`** nests the same way, but `### Part` headings and `---` lines add no URL segment → `(part)/` group folders. Entries pointing at anchors (`page.md#x`) aren't pages.\n- **Still applies:** `{% hint %}` → directives; `{% include \"./x.md\" %}` → `<include>`; `{{ book.name }}` → `variables` and `{{name}}`; other Nunjucks logic → evaluate and inline; `LANGS.md` → `i18n`, with the default language at the content root; `GLOSSARY.md` → no equivalent (report).\n\n## Teardown\n\n- **Turn off Git Sync** for every mapped space before the converted content reaches the synced branch: GitBook imports each push to it and commits its own edits back. Keep the GitBook site published until the domain moves.\n- Delete `SUMMARY.md`, `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, and the emptied `.gitbook/`.\n- Check CI and scripts that read `.md` files or `.gitbook/assets/`: a Markdown link checker breaks on root-absolute routes and skips `.mdx` (replace it with `blume validate --strict`), and an image de-duplication script that greps `*.md` would now delete images in use. Remove GitBook agent instructions (an `AGENTS.md` block, a vendored GitBook skill).\n- A synced repo usually has no `package.json`: scaffold one (SKILL.md step 6).\n\n## Dropped — report these\n\nSite themes and styles beyond accent, fonts, and mode; footer group titles and logo; page covers; page and update tags; adaptive content and visitor authentication; GitBook Assistant unless the user adds `ai.assistant`; hint icons you didn't keep; button styles and search/ask buttons; `<mark>` colors; `fullWidth` and column widths; non-YouTube embeds as players; dark card covers and theme-aware images; inline image sizing; `{% openapi-schemas %}` and GitBook `x-*` spec extensions; automatic redirects you couldn't recover; case-insensitive URLs; page-group icons; sidebar positions Blume can't match (parent pages among loose pages, `***` dividers, external links); section groups flattened into tabs; spaces and translations not in the repo; integrations with no adapter; site redirects nobody could export; anything set only in the app that the user didn't supply.\n\n<!-- File: .agents/skills/blume-migrate/references/github-wiki.md (Tokens: ~9187 | Category: anthropic-skill) -->\n# GitHub Wiki → Blume\n\nA GitHub wiki is a git repository beside the main one (`https://github.com/<owner>/<repo>.wiki.git`) that GitHub renders at `https://github.com/<owner>/<repo>/wiki`. It has no config file and no build: every file in a markup format GitHub renders is a page, `_Sidebar.md` and `_Footer.md` add the chrome, and pages link each other with `[[Page Name]]`. Five things set this migration apart:\n\n- **Folders don't count.** A page's URL is its file name, wherever the file sits (`codebase/compiler/Codebase-Compiler-Binder.md` is `/wiki/Codebase-Compiler-Binder`), and relative links and images resolve from `/<owner>/<repo>/wiki/`, never from the file's folder.\n- **`[[Link text|Page Name]]` puts the text first** in Markdown pages. GitHub's docs show the reverse, which is right only in `.mediawiki` pages, and Obsidian's `[[Page|text]]` is the reverse too, so don't point the `obsidian()` source at a wiki.\n- **Blume renders `[[…]]` as literal text.** It warns `BLUME_WIKILINK_UNSUPPORTED` only for a wiki link that names one of the site's pages or has a `|`, so `[[Page that doesn't exist]]` ships with a green build. Every wiki link has to be converted.\n- **The old URLs live on github.com,** which you can't redirect. What you can do there is in URLs and the old wiki below. Never tell the user the old links will redirect, and add no `redirects` for them.\n- **Nothing pins the routes,** so the new site gets clean ones: one flat lowercase route per page, as the wiki had one flat namespace, with the sidebar groups as `(group)/` folders.\n\nScope: Markdown pages fully. Pages in the other formats GitHub renders get converted to Markdown first (Other markup formats). Checked against GitHub's wiki docs, the live rendering of public wikis (microsoft/TypeScript, microsoft/vscode, koreader, ruTorrent, qBittorrent, mpv, gollum, fzf, AdGuardHome) in October 2026, and an end-to-end migration of the microsoft/TypeScript wiki (77 pages). GitHub Enterprise Server wikis use the same files but weren't checked.\n\n## Detect\n\n`blume migrate` can't detect a wiki: name it, `npx blume migrate github-wiki`. A wiki clone has:\n\n- a remote ending in `.wiki.git` (`git remote -v`);\n- `Home.md` (or `Home` in another format) at the root, often `_Sidebar.md` and `_Footer.md`;\n- page files named like URLs (`Getting-Started.md`, `Standalone-Server-(tsserver).md`, sometimes `Roadmap‐2021‐2022.md` with U+2010 hyphens), `[[Page Name]]` links, no config, and usually no `package.json`.\n\n**Not this format:** Docsify keeps a lowercase `_sidebar.md` beside an `index.html` that sets `window.$docsify` (`references/docsify.md`). On macOS and Windows the file system ignores case, so `_Sidebar.md` and `_sidebar.md` are the same name to `ls`, `test -e`, and Node's `existsSync`: check the real case with `git ls-files`.\n\n**Get the pages from the default branch.** `git clone https://github.com/<owner>/<repo>.wiki.git`, then `git remote show origin` names the HEAD branch. GitHub publishes only that branch, so other branches never showed. If the wiki is mirrored from a normal repository (a sync workflow, or a footer saying \"fork it and send a pull request\"), the wiki clone is still what readers saw.\n\n## Where the Blume project goes\n\nThe wiki repository can't host the new site: GitHub runs no Actions in it, serves no Pages from it, and accepts no pull requests. Ask the user where the docs will live:\n\n- **A folder in the main repository** (the usual choice: the docs then change in the same pull requests as the code). Follow `references/monorepo.md` for a code repo's root `package.json`, `dist/`, and workspace.\n- **A new repository**, for docs with their own maintainers.\n\nWithout an answer, convert a copy of the wiki clone in place: `blume.config.ts` and `package.json` at its root, the pages moved into `docs/`, so the user can move the folder as one unit. **Before you start, run `git remote remove origin` in the copy.** Pushing the migrated tree to the wiki would publish `blume.config.ts`, every moved page, and the stubs you prepare later as wiki pages.\n\nWiki history doesn't come along with a plain copy. To keep it for `git log --follow` and blame, bring the wiki in with `git subtree add --prefix=<folder> https://github.com/<owner>/<repo>.wiki.git <branch>` and say so in the report. Every page gets moved and edited either way, so `lastModified: \"git\"` would date every page to the migration commit until it's edited again (Blume follows a rename only when the file didn't change).\n\n## Run the codemod\n\nFrom the copy of the wiki clone (when the docs go into another folder, copy the wiki's files there first and run it from there), before any hand edits:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo>            # report\nnode <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write    # write the route table\n# classify the pages in wiki-routes.json (next section)\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo>           # dry run\nnode <skill>/scripts/github-wiki-codemod.mjs convert --repo <owner>/<repo> --write   # apply\n```\n\nIt's zero-dependency, deterministic, and idempotent (a second `convert` skips the pages it already moved). Add `--mirror <owner>/<repo>-wiki` when a repository mirrors the wiki, and `--emoji emojis.json` (from `curl -s https://api.github.com/emojis`) when pages use `:shortcodes:`.\n\n- **`routes`** lists the pages the way GitHub serves them and writes `wiki-routes.json` (page name → new route) and `wiki-files.json` (page name → file) beside `blume.config.ts`. It prints the files GitHub never served, routes that clash, digit-led routes, non-Markdown pages, and its guesses at moved notices and non-content pages. It never overwrites an existing table: it lists what differs instead.\n- **`convert`** does everything in the Navigation, Content, and Images sections below that the table and `_Sidebar.md` decide: moves each kept Markdown page to `docs/(group)/<route>.md` (`.mdx` when it needs MDX) with its frontmatter, converts wiki links, wiki URLs, alerts, fences, headings, named anchors, and duplicate link definitions, moves the wiki's images beside the pages, and writes every `meta.ts`. It prints the `navigation.featured` entries for `blume.config.ts`, the footer's text, and a `REVIEW` line for everything that needs judgment. **Every `REVIEW` line is a to-do**; the sections below say what to do with each.\n- **`check`** (after `blume build`) and **`stubs`** (for the old wiki) are in URLs and the old wiki.\n\nThe sections below are the rules the codemod applies, for checking its output and for anything you do by hand.\n\n## The page list and the route table\n\n**Which files are pages.** Every file whose extension GitHub renders: `.md`, `.markdown`, `.mdown`, `.mkdn`, `.mediawiki`, `.wiki`, `.textile`, `.rst`, `.rest`, `.org`, `.creole`, `.asciidoc`, `.adoc`, `.asc`, `.pod`, `.rdoc`. The page name is the file name without its last extension (`README.html.md.md` is the page `README.html.md`), with spaces read as `-`. Matching is case-insensitive, `-` and space are the same, and U+2010 is not `-`.\n\n- **Not pages:** `_Sidebar.*` and `_Footer.*`, in any folder (GitHub even took `_Sidebar.html.md` as a sidebar). Every other `_`-prefixed file is a page (`_ [obsolete] Config.rest`), and Blume skips `_` files by default, so the rename matters.\n- **Two files with the same name** in different folders share one URL, and GitHub serves one of them (the root one, on the wiki checked). The other was never reachable: leave it out and report it.\n- **Every other file** (images, PDFs, `LICENSE`) is served raw at `/wiki/<path>`, which redirects to `https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`.\n- **GitHub's own list** is `https://github.com/<owner>/<repo>/wiki/_pages` (every page, each linked at its URL). Compare the table with it.\n\n**Old URLs.** `Home` is `https://github.com/<owner>/<repo>/wiki` (`/wiki/Home` 301s there); every other page is `https://github.com/<owner>/<repo>/wiki/<Page-Name>`, percent-encoded.\n\n**New routes.** Each page's route is its name, lowercased, with apostrophes dropped and every other run of characters that aren't letters or digits turned into one `-` (`Standalone-Server-(tsserver)` → `/standalone-server-tsserver`, `What's-new-in-TypeScript` → `/whats-new-in-typescript`, `tsconfig.json` → `/tsconfig-json`); non-Latin letters stay. `Home` becomes `/`. Give one of two clashing pages another route by hand. A route that starts with digits and a separator (`/5-0-release-notes`) would lose them as an ordering prefix, so `convert` pins it with `slug`.\n\n**Classify every page** by editing its entry in `wiki-routes.json`:\n\n- **Content:** keep the route.\n- **A moved notice** (the whole page says it moved: \"This page has moved to <URL>\"): set its entry to that URL, or to the target page's route (with its `#anchor`) when the URL is another page of this wiki. It becomes no page; links to it go straight to the target. A short page that points elsewhere without saying it moved (\"An introduction can be found in the handbook\") is content: keep it.\n- **Not content:** a mirror repository's `README.md`, `SECURITY.md`, or `CONTRIBUTING.md`, test pages, script fixtures, and pages that only say \"deprecated\" with nowhere to point. Set the entry to `null` and list it in the report (SKILL.md step 2).\n\n`routes` prints its guesses for the moved notices and the non-content pages; confirm each one.\n\n## Config\n\nWikis have no config: map what the repository and the special pages say.\n\n| GitHub wiki | Blume |\n| --- | --- |\n| Repository name | `title` (the project's name, or \"`<Project>` wiki\") |\n| Repository description (About) | `description` |\n| The repository | `navigation.repo: \"https://github.com/<owner>/<repo>\"` for the footer link. Set `github: { owner, repo, branch, dir }` only once the pages live in a repository, so **Edit on GitHub** opens the new file. Leave it unset while the home is unknown, and report it |\n| An image at the top of `_Sidebar.md` | `logo`, with the file in `public/` |\n| External links in `_Sidebar.md` | `navigation.featured`: `convert` prints the array |\n| `_Footer.md` links | `footer.links` |\n| `_Footer.md` text | a copyright or license line → `footer.copyright`, as plain text (Markdown stripped); other text → drop (report), or a `PageFooter` layout slot (`defineComponents`). A \"contribute to this wiki\" footer drops with its link: edit links replace it |\n| \"edited this page … · N revisions\" under each title | drop (report). `lastModified: \"git\"` would show the migration date on every converted page |\n| Wiki search, the Pages list, the per-page outline, the clone URL box | built in, or drop |\n| Site URL | none existed. Ask where the site deploys and set `deployment` (SKILL.md). GitHub Pages: https://useblume.dev/guides/markdown-docs-github-pages, with `deployment: { site: \"https://<owner>.github.io\", base: \"/<repo>\" }` for a project site |\n\n## Navigation: `_Sidebar.md` → `(group)/` folders\n\nGitHub showed two things in the right rail: the **Pages** list (every page, alphabetical) and, under it, `_Sidebar.md` rendered as Markdown. The sidebar becomes folders, and every page from the Pages list stays reachable.\n\n- **Each sidebar group becomes a `(group)/` folder** (SKILL.md): it adds the sidebar group and no URL segment, so every route stays the flat one from the table. Its `meta.ts` has `title` (the label as written) and `pages` (every child in sidebar order). The content root's `meta.ts` lists the groups in sidebar order.\n- **Group labels** are whatever heads a list: a bold line (`**News**`), a heading (`## Contributing`), or an unlinked list item with children (`* Testing`, a nested `(group)/` folder with `display: \"group\"`). A heading that links a page (`# [Tips and Tricks](…/wiki/Tips-and-Tricks)`) makes that page the group's own: a real folder with the page as its `index.md`. A heading that links outside (`## [User documentation](https://…)`) is a `navigation.featured` link.\n- **A page with nested pages under it** becomes a real folder named for its route, holding the page as `index.md`, its children, and a `meta.ts` with `pages` and `display: \"group\"`. Each child gets `slug: <its route without the slash>` so its route stays flat. Without `display: \"group\"`, a nested folder lists after every loose page of its parent group, not where the sidebar had it. Blume shows the parent both as the group header and as its first row; that's expected, so don't hide it.\n- **External links** can't sit inside a generated group, so they move to `navigation.featured` (above the sidebar, on every page); report the move. A link to a heading (`[Installation](…/User-patches#installation)`) has no row in Blume: drop it (the page's outline lists it) and report it.\n- **A page listed twice** stays in its first group; report the other entry. `[[Home]]` in the sidebar drops: the home page is `docs/index.md`, listed first, outside every group.\n- **Pages the sidebar doesn't list** were still in the Pages list: they go in a last group, `(more)/` with `title: \"More pages\"`, `display: \"group\"`, `collapsed: true`, and a `pages` list sorted by title (Blume's own order goes by file name, which differs for names like `'this' in TypeScript`). Pages that shared a wiki folder can get their own collapsed group instead (`(codebase)/`); report the groups you made.\n- **No `_Sidebar.md`:** GitHub showed only the Pages list, so the pages sit at the content root (Blume sorts them by title), or in groups by the wiki's folders if it had any; report the choice.\n- **A `_Sidebar.md` inside a folder** replaced the root one for that folder's pages. Usually it's a translation (`ru/_Sidebar.md` beside `ru/Home.ru.md`): map the folder to `i18n` when the pages pair up, else to a `navigation.tabs` entry, and report it.\n- Prose, images, and HTML lists in the sidebar have no place in Blume's sidebar: move a logo to `logo`, put useful prose on the home page, and report the rest.\n\n## Content\n\n### Page files and titles\n\n- **Each kept page** lands at `docs/(group)/<route>.md`, `Home` at `docs/index.md`. It stays `.md` unless it needs MDX (GitHub alerts, mermaid, math), then it's `.mdx` (MDX hazards below).\n- **`title`** is the page name as GitHub showed it above the page: `-` → space, U+2010 → `-` (`Standalone-Server-(tsserver)` → `Standalone Server (tsserver)`, `'this'-in-TypeScript` → `'this' in TypeScript`), quoted in YAML where needed.\n- **Body H1s.** GitHub drew the name as the title and every `#` in the body below it, so a page's H1s are sections, not its title (`# TypeScript 5.3` on a page named \"API Breaking Changes\"). A leading H1 that repeats the page name (ignoring case and punctuation) is deleted. If the page still has an H1, every heading is demoted one level (`#` → `##`, `##` → `###`, …; `######` stays), and a one-line setext heading becomes ATX one level down (`===` → `##`, `---` → `###`). A setext heading over several lines has no ATX form: `convert` reports it, so rewrite it as one `###` line by hand. Heading ids come from text, so demoting changes none.\n- **`sidebar.label`** is the sidebar's text for the page, when it differs from `title` by more than case and `-` versus space.\n\n### Wiki links\n\nEvery `[[…]]` outside code converts. GitHub leaves `[[ … ]]` in fenced code alone (bash tests), and so does `convert`: it skips fenced (in a quote too) and indented code, inline code spans, `<code>` and `<tt>` elements, and HTML comments. An indented block that holds `[[` gets a `REVIEW` line, since list continuation and code look alike there. A code span that runs across lines isn't seen as code, so check any `REVIEW` line that quotes shell syntax (`[[ -f x ]]`).\n\n- **Inside an HTML block** (a line opening with `<div>`, `<p>`, `<table>`, `<details>`, and the like, up to the next blank line), Markdown shows as text, but gollum, the engine behind GitHub's wikis, resolves `[[…]]` before the Markdown is parsed, so it made a link there: `convert` writes `<a href=\"/page-name\">text</a>` there, and a bare wiki URL too. An image tag there gets a `REVIEW` line: write it as `<img src=\"…\" />` with the relative path the line gives (Blume publishes the file with the page), or move it out of the block. A `#` line inside the block is text, not a heading, so it isn't demoted.\n- **`'[[Page]]`** (an apostrophe right before, none right after) is gollum's escape for literal brackets: `convert` leaves it and reports it. Check the live page, and drop the apostrophe if it showed the brackets.\n- **`\\[\\[Page\\]\\]`** converts as a wiki link, because pandoc writes them that way (Other markup formats), with a `REVIEW` line: on a page that was always Markdown, GitHub showed those brackets, so restore them there.\n\n| GitHub (`.md` pages) | Blume |\n| --- | --- |\n| `[[Page Name]]` | `[Page Name](/page-name)`: the text as written, the route from the table |\n| `[[Link text\\|Page Name]]` | `[Link text](/page-name)` |\n| `[[Page Name#anchor]]`, `[[text\\|Page Name#anchor]]` | `[…](/page-name#anchor)`, the anchor as written |\n| `[[text\\|https://…]]` | `[text](https://…)` |\n| `[[Home]]` | `[Home](/)` |\n| a page the wiki doesn't have (GitHub drew it as a link to create the page), or one set to `null` | plain text; reported |\n| a moved notice | a link to its target |\n| `[[image.png]]`, `[[/images/x.png\\|alt=Text]]`, `[[x.png\\|align=center,frame,alt=Text]]`, `[[https://…/x.png\\|alt=Text]]` | an image: `![Text](…)` (Images and files). Any first part with an image extension is an image, whatever follows; options are separated by `,` or `\\|`. A leading `/` is the wiki root. A path the root doesn't have but the page's own folder does resolves there, reported. Options other than `alt=` (`width=`, `align=`, `frame`, stray text) drop; reported |\n| `[[text\\|docs/spec.pdf]]` (a file stored in the wiki) | left as is and reported: copy the file into `public/` and link its root path |\n| `[[_TOC_]]` | deleted: GitHub lists a table of contents among unsupported syntax, and Blume's outline shows the headings |\n\n(In this table `\\|` is the table's escape for a single pipe.) GitHub trims spaces around the pipe (`[[Mailing list | https://…]]`), and inside a Markdown table cell the pipe may be written `\\|`. Pages are looked up the way GitHub did: by name, ignoring case, with `-` and space interchangeable and folders ignored. **Anchors are case-sensitive in Blume** and weren't on GitHub, wherever they appear: when `blume validate` reports one (`#TSDeclFiles` for a heading pinned `[#TsDeclFiles]`), match the id's case.\n\n### Markdown links\n\n| Link | Blume |\n| --- | --- |\n| `https://github.com/<owner>/<repo>/wiki/Page-Name` (any case of owner and repo, `http`, `www.`), `/<owner>/<repo>/wiki/Page-Name`, or bare `Page-Name` / `./Page-Name` | the page's route through the table, keeping `#anchor`, percent-decoded (`%28` is `(`) |\n| `…/wiki`, `…/wiki/`, `…/wiki/Home` | `/` |\n| a mirror repository's copy of a page (`https://github.com/<owner>/<repo>-wiki/blob/<branch>/Page.md`) | the page's route (`--mirror`): the mirror goes stale with the wiki |\n| a bare wiki URL in prose, or `<https://…/wiki/Page>` | `[Page title](/route)` |\n| a page's file by its path (`dir/Page.md`, `…/wiki/dir/Page.md`), which GitHub served raw | the page's route; reported |\n| `#user-content-x` | `#x` (GitHub's ids carry that prefix, so both forms worked there) |\n| relative links that leave the wiki (`../issues/12`, `../blob/main/src/x.ts`, `../../other-repo`) | the absolute URL each resolved to from `https://github.com/<owner>/<repo>/wiki/<page>` (`../issues/12` → `https://github.com/<owner>/<repo>/issues/12`) |\n| a link to a page that doesn't exist (`./formatter.md`, `GLOSSARY.md#x`: GitHub served nothing there) | left as is and reported: it was broken on GitHub too, so retarget it (often the page moved to another repository) or unlink it |\n| `…/wiki/_pages`, `…/wiki/<Page>/_history`, revision URLs | left and reported: they point at the wiki's UI and stop working with it |\n| another repository's wiki | unchanged |\n\n`#123` and `@user` are plain text in wikis (GitHub doesn't autolink them there), so they stay. Bare URLs autolink in both. A raw HTML `<a href>` to a wiki page gets the route too (and a wiki link in an HTML block becomes one); Blume adds `deployment.base` to its root path like a Markdown link's.\n\n### Link definitions\n\n`blume validate` checks every link reference definition the page uses, including ones nothing cites; when a label is defined twice it checks only the first, which CommonMark uses. Wikis kept by scripts collect definitions that never rendered:\n\n- **A later definition of a label** never rendered: `convert` deletes it.\n- **A Foam or VS Code block** of autogenerated definitions (`[//begin]: # \"Autogenerated link references…\"` … `[//end]: #`) goes too: once the wiki links are converted, nothing uses it.\n- **A line like `[1]: <src/file.ts - function x(`** with no closing `>` isn't a definition at all: it shows as text, on GitHub and in Blume alike. Leave it, or delete it if it's leftover tooling syntax, and report it.\n\n### GitHub Markdown\n\n| GitHub | Blume |\n| --- | --- |\n| `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` at the start of a line | `:::note`, `:::tip`, `:::note`, `:::warning`, `:::danger`, in `.mdx`. Blume renders an alert itself as the same callout in `.mdx`, so `convert` leaves one indented in a list, or with lazy lines, as written and makes the page `.mdx`. One with text after its marker, or no body, is reported: convert it by hand. In a `.md` page an alert renders as a quote with a literal `[!NOTE]` and warns `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. The older `> **Note**` form is a plain quote on GitHub today: leave it |\n| ` ```mermaid ` | the same fence, in `.mdx` |\n| ` ```math ` | a `$$` block, in `.mdx` |\n| `$…$`, ``$`…`$``, `$$…$$` | by hand: `$$…$$` in `.mdx` (inline ones stay in their sentence; SKILL.md \"Math\") |\n| ` ```geojson `, `topojson`, `stl` | a code block: maps and 3D models drop; reported |\n| Fence languages | lowercased (`TypeScript` → `typescript`), which only tidies: Blume reads them in any case, as GitHub did. `posh` → `powershell`. Any other name Shiki lacks renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE`: change it to a Shiki language, or `text` |\n| `:emoji:` shortcodes | the character, with `--emoji`. GitHub slugged a heading's shortcode name, so a heading with one is reported: convert it and pin the old id (`## ⚠️ Known issues [#warning-known-issues]`) |\n| `<a name=\"x\"></a>` in a heading | deleted, with `[#x]` pinned on the heading (Blume would give the heading the id `x` anyway). Elsewhere it stays: links to `#x` work, and `blume validate` resolves them |\n| task lists, tables, footnotes, strikethrough, autolinks, `<details>` | unchanged |\n| `style=`, `<script>`, `<iframe>`, `<form>`, `<input>` | by hand: GitHub's sanitizer removed them, so readers never saw them, while Blume renders raw HTML as written. Delete them, or keep one deliberately and report it |\n\nHeading ids need no pinning beyond the rows above: GitHub and Blume both slug with `github-slugger` (identical ids for all 710 headings on the wiki pages checked). The exception is a heading that ends in an emoji or symbol after a space: GitHub keeps the trailing dash (`## Features ✨` → `#features-`), Blume drops it (`#features`); `blume validate` reports the links to fix. Deleting a leading H1 can shift the `-1` suffix of a later heading with the same text; `blume validate` reports the links that break. `pin-heading-ids.mjs` can't read a wiki anyway: GitHub puts each id on a link beside the heading, not on the heading.\n\n### MDX hazards\n\nMDX rejects things GitHub's Markdown accepts, and a page renamed to `.mdx` fails `blume build` on them. In prose (outside code), `convert` fixes the mechanical ones in the pages it renames: a `<` that doesn't start a tag (`TypeScript >=1.6, <7` → `&lt;7`), bare `{` and `}` (escaped `\\{`), HTML comments (`{/* */}` on one line), unclosed void tags (`<br>` → `<br />`), `<https://…>` autolinks, and indented code (MDX has none, so a top-level indented block gets a fence; a backslash-escaped backtick doesn't start inline code, so braces after it get escaped too). It reports the rest:\n\n- **A tag-like word** (`Array<string>` outside code, `<placeholder>`): MDX reads it as an unclosed JSX element. Put it in backticks.\n- **Raw HTML blocks** (`<details>`, `<div>`, `<table>`) with Markdown inside: MDX parses that Markdown only with blank lines around it, and fails on tags interleaved with list items. Add the blank lines, or turn the block into Markdown.\n- **Indented code inside a list:** fence it by hand, at the list item's indentation.\n\n### Other markup formats\n\nConvert each non-Markdown page to Markdown before `convert` (which skips and lists them): `pandoc -f <format> -t gfm-raw_html --wrap=preserve page.<ext> -o page.md` (`mediawiki`, `textile`, `rst`, `org`, `creole`, `asciidoc`, `pod`). `.rdoc` has no pandoc reader: convert it by hand. Delete the original, point the page's entry in `wiki-files.json` at the new `.md` file, and rerun `convert`.\n\n- **MediaWiki pages put the target first,** `[[Page Name|text]]`, as MediaWiki does. Pandoc turns them into `[text](Page_Name \"text\")`, with `_` for spaces, and `convert` maps those to routes.\n- **In the other formats** pandoc escapes `[[…]]` to `\\[\\[…\\]\\]`, and `convert` reads those as Markdown wiki links (text first).\n- Heading ids differ in these formats. Compare each converted page with the live one, and list every conversion in the report.\n\n## Frontmatter\n\nGitHub reads no frontmatter, so wiki pages have none. Each page gets `title`, plus `sidebar.label` and `slug` where the sections above say so. If a page opens with a `---` block anyway, check the live page for what readers saw, keep that as content or drop it, and report it.\n\n## Images and files\n\n- **Files stored in the wiki** (`https://raw.githubusercontent.com/wiki/<owner>/<repo>/<path>`, `https://github.com/<owner>/<repo>/wiki/<path>`, or a relative `<path>`, which resolves from the wiki root whatever the page's folder) go away with the wiki. `convert` moves each referenced one into `docs/` at the same path (`docs/images/x.png`) and points the page at it relatively (`../images/x.png` from a `(group)/` page): Blume optimizes and validates relative Markdown images.\n- **A relative image that pointed at nothing** (it climbs out of the wiki root, like `../../screenshots/x.png`, or names a file the wiki doesn't have) was broken on GitHub too, and fails `blume build`: `convert` drops it and reports it.\n- **Uploaded attachments** (`user-images.githubusercontent.com`, `github.com/user-attachments/assets/…`, `private-user-images.githubusercontent.com`) are served outside the wiki and keep working for a public repository. `convert` reports each one: download and vendor it next to the pages (one request at a time) unless the user objects. From a private or internal repository they need a GitHub login, so vendoring is required.\n- **`https://github.com/<owner>/<repo>/blob/<ref>/<path>`** shown as an image is an HTML page anywhere but GitHub: `convert` uses `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`.\n- **Raw `<img>` alone on its line** becomes a Markdown image; its `width`/`height`/`align` drop and are reported. A raw `<img>` of a wiki file inside other HTML (a table cell, a `<p>`) is reported: its `src` still reads from the wiki root, which isn't the page's new folder. Copy the file into `docs/` at the same path, as `convert` does for the others, and point `src` at it relative to the page (`../images/x.png`), which Blume publishes with the page, unoptimized; or make it a Markdown image.\n- **Linked files** (PDFs, archives, an image opened as a link) are reported: copy each into `docs/` at the same path and link it relative to the page, which publishes it with the page, or into `public/` and link its root path for a URL that never changes.\n- `#gh-dark-mode-only` / `#gh-light-mode-only` pairs: keep the light image (report).\n- Files nothing references stay in the wiki (other sites may hotlink them): report them, don't copy them.\n\n## URLs and the old wiki\n\nEvery old URL is on github.com. GitHub has no redirect setting for wiki pages, and it sanitizes their HTML (no `<script>`, no `<meta>` refresh), so nothing on the old wiki can forward a reader. Don't add Blume `redirects` for them either: they would only match paths on the new site. The route table is the record instead: each old page name maps to its new route, a target URL, or `null`.\n\n**Check the table against the build** after `blume build`: `node <skill>/scripts/github-wiki-codemod.mjs check` confirms every route in the table exists in `dist/` (`dist/client/` on a server build), anchors included. Given the old URLs instead (every line of a saved `_pages` list, for one), it prints each one's new route: `ok`, `moved` for a moved notice's outside target, `DROPPED` for a page set to `null`, or `MISSING` for a page the table lacks.\n\n**What the user can do on the old wiki** (prepare it, but never commit or push it, even with push access: `stubs` only writes files, and whether and when anything goes to `<repo>.wiki.git` is the user's decision, after the new site is live):\n\n1. **Restrict editing** (Settings → Features, under Wikis: **Restrict editing to collaborators only**) as soon as the migration starts, so the content stops drifting.\n2. **Stop anything that syncs the wiki, before pushing anything to it.** Otherwise it reverts the stubs or copies them on. Look for a workflow triggered `on: gollum` (wiki edits) in the main repository, a mirror repository (`<repo>-wiki`) whose workflow pushes to `<repo>.wiki.git` on push or on a schedule, a copy of that workflow inside the wiki itself, and wiki-publishing actions. List every one you find.\n3. **Point readers at the new site:** a notice at the top of `Home` and `_Sidebar.md` (the sidebar shows on every page). In a fresh clone of the wiki (not the Blume project: `stubs` refuses to run beside `blume.config.ts`), with `wiki-routes.json` and `wiki-files.json` copied in (don't commit them there): `node <skill>/scripts/github-wiki-codemod.mjs stubs --site https://docs.example.com --notice-only --write`.\n4. **Or replace every page with a link stub,** so deep links from issues, search results, and bookmarks land one click from the new page: the same command without `--notice-only`. Each page keeps its file name, so its URL keeps working; `_Footer` files go, `_Sidebar.md` becomes the notice, images stay for sites that hotlink them, and pages set to `null` are listed for the user to decide. `--site` is the new site's URL with its base path, so regenerate the stubs once the real URL is known.\n5. **Disable the wiki** (Settings → Features → Wikis), once the repository's own links point at the new site. GitHub hides every page, stubs included, without erasing them. It's the user's call; it's never required.\n\nWhether search engines matter: `curl -sI https://github.com/<owner>/<repo>/wiki | grep -i x-robots-tag` prints `none` for a wiki GitHub keeps out of search results (GitHub indexes only wikis with 500+ stars and editing restricted to collaborators). Without it, search results keep sending readers to the old pages for a while, and the stubs are what they find.\n\n**Inbound links in the main repository.** Search its clone with `git grep -n -i -E 'github\\.com/<owner>/<repo>/wiki'`: README, CONTRIBUTING, `docs/`, `.github/ISSUE_TEMPLATE/`, `SECURITY.md`, code comments and test fixtures, `package.json` `homepage`. A large repository doesn't need a full clone: `git clone --depth 1 --filter=blob:none --sparse <url>`, then `git sparse-checkout set --no-cone '/*' '!/tests/baselines/'` (excluding its biggest generated folders). Map each URL through the table with `check`, which also verifies its anchor against the build and suggests ids for one that's gone:\n\n```bash\nnode <skill>/scripts/github-wiki-codemod.mjs check https://github.com/<owner>/<repo>/wiki/FAQ#common-bugs\n# MISSING ANCHOR  …/wiki/FAQ#common-bugs → /faq#common-bugs (ids like: common-bugs-that-arent-bugs)\n```\n\nAn old link can carry an anchor that was already dead on GitHub: fix it to a real id, don't copy it. Rewrite each link with the new site's origin (or ask first, when the user only wants a report), and set the repository's About → Website to the new site if it pointed at the wiki. Links in issues, pull requests, and other repositories can't be rewritten: the stubs catch them. List the files you changed or would change.\n\n### Leftover check\n\nRerun until it prints only code samples you mean to keep:\n\n```bash\ngrep -rnE '\\[\\[|user-content-|(^|\\s):[a-z0-9_+][a-z0-9_+-]*:(\\s|$)' docs\ngrep -rniE 'raw\\.githubusercontent\\.com/wiki/|github\\.com/<owner>/<repo>/wiki' docs   # any case of owner and repo\n```\n\nAn alert left in a `.md` page warns `BLUME_MD_GITHUB_ALERT`, and an unknown fence language `BLUME_UNKNOWN_CODE_LANGUAGE`, so `blume build` lists those.\n\n## Teardown\n\n- **Files left behind.** Converting in place leaves the pages you didn't keep where they were: moved notices, `null` pages, the files GitHub never served, and unreferenced images. `convert` lists them. Delete them from the project (nothing under the root publishes, but they confuse later edits): the wiki keeps them, and `wiki-files.json` records their paths for the stubs. Delete `_Sidebar.md` and `_Footer.md` once `convert` has harvested them (a later `convert` then leaves the `meta.ts` files as they are).\n- **Wiki tooling** (a `package.json` for a Markdown generator, link-rewriting scripts, sync scripts and workflows) stays only if it still applies to the pages: run each script in its dry or check mode against `docs/` and see whether any page still uses the syntax it converts. If none does, it goes, with the dependencies only it used. Say which you kept.\n- **Lockfiles:** a mirror repository's `.gitignore` often ignores `package-lock.json` or `yarn.lock`. Remove those lines, so the lockfile SKILL.md step 6 regenerates gets committed.\n- Keep `wiki-routes.json` and `wiki-files.json` beside `blume.config.ts` until the stubs are pushed and the inbound links are rewritten.\n- Scaffold `package.json` (SKILL.md step 6) if the docs' new home has none.\n\n## Dropped — report these\n\nThe Pages list and the clone box; footer text other than a copyright line, and its link; sidebar prose, images, anchor links, and duplicate entries; external sidebar links moved to `navigation.featured`; the per-page \"edited … · N revisions\" line; image sizes and wiki image options; images that were broken on GitHub; maps and 3D models; theme-aware image pairs; raw HTML GitHub had stripped; links to missing pages (GitHub's create-page links) and links that were already broken; shadowed link definitions; pages left out (mirror files, tests, fixtures, deprecated stubs) and moved notices mapped to their targets; pages GitHub never served (same-name shadows); non-Markdown pages converted with pandoc; wiki-UI links (history, revisions, `_pages`); and every old URL, which stays on github.com: say what you prepared for it (notice, stubs, or neither), which sync workflows must stop first, and that nothing redirects.\n\n<!-- File: .agents/skills/blume-migrate/references/jekyll.md (Tokens: ~8919 | Category: anthropic-skill) -->\n# Jekyll (Just the Docs) → Blume\n\nJekyll is a Ruby static site generator: a `_config.yml`, Markdown pages with front matter, Liquid templating (`{% … %}`, `{{ … }}`) in every page, and Kramdown as the Markdown engine. Docs sites get their navigation from a theme, and the common docs theme is **Just the Docs** (JTD). The work is turning JTD's front-matter navigation into folders and `meta.ts` without moving a URL, and converting the Kramdown and Liquid in every page. Watch for six traps:\n\n- **Navigation lives in front matter** (`parent`, `nav_order`), not in folders. URLs come from the file path or `permalink`, so the nav tree and the file tree often disagree.\n- **Kramdown attribute lists (`{: .note }`) and Liquid (`{% include %}`, `{{ site.x }}`) render as literal text in a Blume `.md` page, with a green build.** Each warns at its line (`BLUME_TEMPLATE_TAG` for Liquid, `BLUME_MD_ATTRIBUTE_LIST` for an IAL). In `.mdx` both fail the build, and `blume check` names each line first: `BLUME_MDX_SYNTAX` for Liquid, `BLUME_MDX_ATTRIBUTE_LIST` for an IAL. Callouts are IALs, so most pages need converting.\n- **URLs end in a slash** (`permalink: pretty`, `/a/b/`) or in `.html` (the default), and relative links were written against those URLs.\n- **The page heading is the body `# H1`.** Front matter `title` is the nav label and `<title>`.\n- **Callout names are the site's own**, declared in `_config.yml`. An IAL whose name isn't declared, or is misspelled, rendered a plain paragraph.\n- **What a page shows depends on Liquid and CSS that ran around it** (`jekyll.environment`, `{% if %}`, `_data`, rules in `_sass/custom` that hide sections or add text). The built site is the record of what readers saw.\n\nScope: **Jekyll 4.x with Just the Docs** (gem or `remote_theme`) is supported, and so is a JTD site that GitHub Pages builds with its own Jekyll 3 (differences flagged below). Checked against Jekyll 4.4.1, Just the Docs 0.12.0 (navigation as of 0.10: unlimited depth, `ancestor`), kramdown 2.5.2 with kramdown-parser-gfm 1.1.0, jekyll-redirect-from 0.17.0, GitHub Pages' gem set (`github-pages` 232, Jekyll 3.10.0), and an end-to-end migration of artineering-io/flair-docs (61 pages). **Plain Jekyll and other doc themes are manual** (see the end): the codemod's Kramdown, Liquid, link, and redirect passes apply to any Jekyll site; only the navigation is JTD's.\n\n## Detect\n\n- **`_config.yml`** at the root or in `docs/` (GitHub Pages' \"deploy from `/docs`\"). It's generic Jekyll.\n- **Just the Docs:** `theme: just-the-docs` or `remote_theme: just-the-docs/just-the-docs` (also the old `pmarsceill/just-the-docs`, with or without `@ref`) in `_config.yml`, or `gem \"just-the-docs\"` in the `Gemfile`. Pages carry `nav_order`/`parent`.\n- **Not Jekyll:** a `_toc.yml` beside `_config.yml` is Jupyter Book; `hexo` in `package.json` is Hexo. A `_config.yml` holding only `theme: jekyll-theme-*` publishes a repo's README with GitHub Pages: one page.\n- **Which Jekyll:** a `Gemfile` with `gem \"jekyll\", \"~> 4…\"` is 4.x. `gem \"github-pages\"`, or no `Gemfile` with a `remote_theme`, is GitHub Pages' Jekyll 3 (3.10 today), which also forces nine plugins on (`jekyll-optional-front-matter`, `-readme-index`, `-titles-from-headings`, `-relative-links`, `-default-layout`, `-github-metadata`, `-gist`, `-paginate`, `-coffeescript`): front-matterless Markdown files are pages, `README.md` is its folder's index when there's no `index.md`, an untitled page takes its first heading as `title`, and `[x](page.md)` links work.\n\n**Where the Blume project goes:** in the Jekyll source directory (the folder holding `_config.yml`), beside `_includes/` and the pages. Set `content.root: \".\"` and scope `content.include` to the page files (`[\"*.{md,mdx}\", \"docs/**/*.{md,mdx}\"]`), and list Jekyll's own `exclude` Markdown (`README.md`, `CHANGELOG.md`) plus `vendor/**` and `node_modules/**` in `content.exclude`, which adds to the defaults `\"**/_*\"` and `\"**/.*\"` (`references/monorepo.md` §1). `_includes/` and `_data/` stay unpublished because of their underscore. In a code repo whose Jekyll source is `docs/`, that puts `package.json` in `docs/` and keeps `dist/` out of the repo root.\n\n## Build the old site first\n\nThe old build is your URL list, your nav tree, your heading ids, and the record of what rendered. Build a copy in a folder outside the repo (`<old>` below, an absolute path), with a project-local bundle. CI usually sets `JEKYLL_ENV=production`, which switches analytics and any `jekyll.environment` branches in includes. From the Jekyll source directory:\n\n```bash\nrsync -a --exclude .git --exclude _site ./ <old>/src/ && cd <old>/src\nbundle config set --local path vendor/bundle && bundle install\nJEKYLL_ENV=production bundle exec jekyll build -d <old>/site\ncd <old>/site && find . -name '*.html' -not -path './assets/*' | sed 's|^\\.||; s|/index\\.html$|/|' | sort > <old>/old-urls.txt\n```\n\nNo `Gemfile` (a GitHub Pages `remote_theme` site): write one with `gem \"github-pages\", group: :jekyll_plugins`. Recent Rubies moved `csv`, `base64`, `bigdecimal`, and `logger` out of the default gems, so under Bundler an older Jekyll fails to load them: add the one it names to the copy's `Gemfile`. If no build works, use the live site (`pin-heading-ids.mjs --old <url>`) and its `sitemap.xml`, rebuild the nav by hand, and say so.\n\nIn `old-urls.txt`, a slashed entry is a page. An `.html` entry is a page on a non-`pretty` site, `404.html`, or a jekyll-redirect-from stub, which GitHub Pages serves without the `.html` (`/old` from `old.html`): `redirects.json` in the build lists those. Non-page files Jekyll copied (`LICENSE`, `.bat` scripts, a `Rakefile`) were published by accident unless a page links them: leave them out and report them. On Jekyll 4, a `.md` file with no front matter wasn't a page either: Jekyll copied it raw.\n\n## Run the codemod\n\nFrom the Jekyll source directory, dry-run the bundled codemod against the old build, then apply it once, before any hand edits:\n\n```bash\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site .          # report only\nnode <skill>/scripts/jekyll-codemod.mjs --old <old>/site --write .\n```\n\nIt's zero-dependency and deterministic, and it runs once: `--write` always writes `jekyll-migration.json` and refuses a folder that already has one, because a second pass would take each page's next H1 as its title and convert what `{% raw %}` protected. To run it again, start from a fresh copy of the Jekyll source. It reads `_config.yml` (`callouts`, `url`, `baseurl`, `permalink`, `defaults` permalinks, `exclude`, plain values for `{{ site.x }}`) and every page and Markdown partial in `_includes/`, and converts what maps exactly:\n\n- callout IALs → directives (Callouts below), `.label` → `<Badge>`, `.no_toc` and `{#id}` on headings → `[!toc]` and `[#id]`, and JTD's utility and button classes dropped; a malformed IAL, or one naming a class `callouts:` doesn't declare, rendered a plain block, so it goes and is reported;\n- the in-page TOC (the list, its `<details>`, its heading, a partial holding only a TOC), `{::comment}`, ALDs;\n- the body H1 → `title` (a different old `title` → `sidebar.label` and `seo.title`), with a variable it showed written in as its value, since Blume leaves front matter as written; an H1 inside an HTML comment or `<pre>` is skipped. The front matter keys follow the table below;\n- `{% include x.md k=\"v\" %}` and `{% include_relative %}` → `<include k=\"v\">/_includes/x.md</include>` (nested ones too, `{{ include.k }}` → `{{k}}`); a parameter the partial reads but the tag leaves out printed nothing in Jekyll, so it's passed as `k=\"\"`. Also `{{ site.x }}` → `{{x}}` variables, `{{ site.baseurl }}`, `relative_url`, `absolute_url`, `{% link %}`, `{% highlight %}`, `{% comment %}`, and `{% raw %}` (unwrapped, its content untouched);\n- links: relative links resolved against each page's old URL, `.md`, `.html`, own-origin, and trailing-slash links → routes; relative Markdown images rebased from the page's new folder, and a relative raw HTML `src` → a root path (Jekyll resolved it against the page's old URL, which the new folders don't mirror);\n- with `--old`, the sidebar the old build rendered → folders, `slug`s, and `meta.ts` files (Navigation below);\n- each page that now needs MDX → `.mdx`, with void tags self-closed, comments as `{/* */}`, a bare `<` as `&lt;`, autolinks as links, and page `<script>`/`<style>` removed, inline code left as written; partials such a page includes get the same;\n- `redirect_from`, `redirect_to`, `.html` URLs, and the variables → `jekyll-migration.json`, for the config:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nimport migration from \"./jekyll-migration.json\" with { type: \"json\" };\n\nexport default defineConfig({\n  redirects: migration.redirects,\n  variables: migration.variables,\n});\n```\n\n**Every `REVIEW` line is a to-do**, and the sections below say what to do with each: HTML includes, Liquid logic, unknown front matter, MDX hazards (a brace in prose, an indented code block), the CSS rules that hid or added content, and the theme files. A `{{ name }}` that `{% raw %}` kept as written collides with a variable of the same name, which Blume fills in everywhere, code included: rename the variable (its key in `jekyll-migration.json` and the `{{name}}` references the codemod wrote). A site whose `_config.yml` has no `callouts:` (before JTD 0.4 added the option, sites styled `.note`/`.warning` in `_sass/custom/custom.scss`) keeps its callout IALs, and the dry run says so: read those rules for the names, then pass `--callouts note,warning` on the write run.\n\nAfter the first `blume build`, pin the old heading ids, rebuilding until it reports 0. Jekyll's GFM ids drop underscores (`f_position` → `#fposition`) where Blume keeps them, so about one id in twenty differs:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old>/site --write\n```\n\n## Config: `_config.yml` → `blume.config.ts`\n\nApply `defaults:` (front matter defaults) to the pages in each `scope.path` that the codemod didn't see through: they often set `layout`, and sometimes `parent` or `nav_exclude`.\n\n| Jekyll | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `url` | `deployment.site`, unless the target is Vercel or Netlify (SKILL.md). GitHub Pages with a custom domain needs it |\n| `baseurl` (`/repo`) | `deployment: { base: \"/repo\" }`. **The deployed URL decides:** the GitHub Pages starter workflow passes `--baseurl` from the Pages settings, so a custom domain (a `CNAME`) serves at the root whatever `_config.yml` says |\n| `exclude` / `include` | `content.exclude` / `content.include` (Detect) |\n| `permalink`, `defaults`, `collections` | URLs; above; Collections and posts |\n| `plugins` | Plugins |\n| `kramdown.syntax_highlighter_opts.block.line_numbers: true` | no site-wide switch: drop it and report it. Code without line numbers copies cleanly |\n| `kramdown.toc_levels`, and pages built on H4 sections | Blume's outline shows H2–H3, where Kramdown's TOC listed every level. Count the H4s: when they carry the page (attribute references), set `toc: { maxHeadingLevel: 4 }`; else report it |\n| other `markdown`, `kramdown`, `highlighter`, `sass`, `compress_html`, `liquid` keys | drop |\n| `github_username`, `twitter`, `social.links`, a Discord link | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube) |\n| `repository` (jekyll-github-metadata) | `github: { owner, repo }` |\n| `logo` | `logo`, file moved to `public/`. JTD shows the logo _instead of_ the title, so a wordmark needs `logo: { image, text: \"\" }`; a custom `_includes/title.html` that shows both keeps the text |\n| `favicon_ico` | the file at `public/favicon.ico` (no config field) |\n| `color_scheme: dark` | `theme.mode: \"dark\"`. `light`/unset → omit. A custom scheme (`_sass/color_schemes/<name>.scss`): its `$link-color` → `theme.accent` (`$blue-000` is `#2c84fa`; look the rest up in the theme's `_sass/support/_variables.scss`), and an `@import \"./color_schemes/dark\"` in it → `theme.mode: \"dark\"` |\n| `aux_links` (`\"Label\": \"url\"` or `[\"url\"]`) | `navigation.actions: [{ label, href }]`. `aux_links_new_tab` → drop (Blume opens `http(s)` header links in a new tab) |\n| `nav_external_links` (`title`, `url`) | `navigation.featured` (above the sidebar, where JTD listed them below the pages) |\n| `search_enabled: false` | `search: false`. `search.*` options → drop |\n| `nav_enabled: false` | no site-wide switch: `mode: center` on the pages that showed no sidebar |\n| `mermaid` | delete; ` ```mermaid ` renders natively in `.mdx` |\n| `ga_tracking` | `googleAnalytics({ id })` from `blume/analytics`, one per `G-` id in the comma list. A `UA-` id no longer collects data: report it |\n| `gh_edit_link` + `gh_edit_repository` + `gh_edit_branch` | `github: { owner, repo, branch }`. `gh_edit_source: docs` → `github.dir: \"docs\"` when `blume.config.ts` sits there |\n| `last_edit_timestamp` | `lastModified: \"frontmatter\"` when pages set `last_modified_date` (the codemod maps it), else drop: JTD showed no date without it |\n| `footer_content` / `_includes/footer_custom.html` | links → `footer.links`, social links → `footer.socials`; copyright text → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`), and turn a Liquid year (`{{ \"now\" \\| date: \"%Y\" }}`) into `${new Date().getFullYear()}` in a template literal; other text → drop (report) or a `Footer` layout slot (`defineComponents`) |\n| `callouts`, `heading_anchors`, `enable_copy_code_button`, `back_to_top*`, `nav_sort`, `nav_error_report`, `ga_tracking_anonymize_ip`, `gh_edit_link_text`, `gh_edit_view_mode` | drop (the codemod read `callouts`) |\n\n## Navigation: front matter → folders and `meta.ts`\n\nJTD builds one tree from `title`, `parent`, `grand_parent`/`ancestor`, and `nav_order`, wherever the files sit, and shows it as a collapsible tree with only the current section open: set **`navigation.sidebar.display: \"group\"`**. The codemod rebuilds the tree from the sidebar the old build rendered, so `nav_order` ties, `child_nav_order: reversed`, and `nav_sort` are already resolved, and keeps every URL:\n\n- A parent at `dir/index.md` with its children in `dir/` (the layout JTD's docs recommend) moves nothing.\n- A parent that's a leaf file with its children beside it (`docs/setup.md`, `docs/install.md`) becomes a `(group)` folder: `docs/(setup)/index.md` with `slug: docs/setup`, and the children inside it keep their URLs.\n- A child whose file sits elsewhere moves under its parent with `slug: <old route>`. A child _section_ whose folder sits elsewhere is reported: `git mv` it into the parent's folder (into a `(group)` folder, its URLs stay; into a real one, pin each page's `slug`), or leave it. The parent's `meta.ts` already lists it by its folder name.\n- **A `parent` title that names two pages** (two sections both titled \"Setup\", told apart by neither `grand_parent` nor `ancestor`): JTD listed the child under both. Blume lists a page once, so it stays under the first and the codemod reports it.\n- Each folder gets a `meta.ts`: `pages` in the old order; `title` only where Blume's label for the folder (its name, humanized) differs from JTD's; `directory: \"accordion\"` where the old parent page listed its children (JTD's `has_toc`, read from the old build), and `\"none\"` below it where a page didn't, since `directory` inherits.\n- **The duplicate index row:** Blume links a folder's `index` from the group row and lists it again as the first row, where JTD showed it once. The codemod hides that row (`sidebar.hidden: true`); a page the group row links stays in the sitemap, search, and `llms.txt`. `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`, flags a hidden index whose `title` differs from its folder's `meta.ts` `title`, unless its `sidebar.label` names the folder title. An index whose H1 differed from its JTD title gets that title as `sidebar.label`, which is the section label, so it passes too. Where neither the page's `title` nor its `sidebar.label` matches the section label (JTD always labeled a section with its page's `title`, so this is rare), the codemod leaves the row visible and says so. The check accepts three ways out: match the two titles, set the page's `sidebar.label` to the folder title, or show the row (remove `sidebar.hidden`).\n- Pages outside the old sidebar (`nav_exclude`, no `title`, a `parent` that matched nothing) get `sidebar.hidden: true`, which also takes them out of search, the sitemap, and `llms.txt`, where JTD still searched them: set `search: { indexing: { includeHiddenPages: true } }` if they should stay searchable. A hidden home page is the exception: the root URL serves it, so it stays listed. An external `nav_external_links` entry is listed for `navigation.featured`.\n\nWhat's left for you:\n\n- **A wrapper folder JTD never showed** (the template's `docs/`, any folder whose pages sit at the nav's top level) becomes one group around them. Blume opens a group that's the sidebar's only top-level row by itself, but one beside the home page's row or another section starts closed off its own pages, so the codemod writes `collapsed: false` in its `meta.ts`. A `navigation.tabs` entry instead lifts its pages to the sidebar's top level, but there pages list above groups, so JTD's interleaved order breaks. Keep the group unless the user asks for a tab.\n- **The home page** (`index.md`, often `layout: home` and untitled) stays the content root's `index`. When the old nav never showed it, the codemod hides its row like any page outside the nav; a hidden home page stays in the sitemap, search, and `llms.txt`. Give it a `title` (Blume renders it as the H1, where JTD showed none) or `mode: frame` to keep the sidebar without one. Where the home page headed a JTD section, Blume's root index can't head a group: its children stay in their folders, and the codemod reports it.\n- **Moving a page changes what its relative images resolve against.** The codemod rebases the Markdown images it converted, and turns a relative raw HTML `src` into a root path, with a REVIEW to serve that file from `public/`.\n\n## URLs and redirects\n\nA Jekyll page's URL, for `dir/name.md`:\n\n| `permalink` (global) | `dir/name.md` | `dir/index.md` | Blume route |\n| --- | --- | --- | --- |\n| `pretty` | `/dir/name/` | `/dir/` | `/dir/name`, `/dir` |\n| unset (`date`), `none`, `ordinal` | `/dir/name.html` | `/dir/` | same |\n| a style ending in `/` / in `:output_ext` / neither | `/dir/name/` / `.html` / `/dir/name` (file `name.html`) | `/dir/` | same |\n\nA page's own `permalink:` wins, then one `defaults:` sets for its folder (the most specific `scope.path`), then the global style. The codemod applies all three, and with `--old` it checks each URL against the build: a page the build doesn't have at that URL gets a REVIEW and no `.html` redirect. Blume serves `<route>/index.html`, so `/dir/name/` keeps working on static hosts, and `blume dev` and `preview` redirect it to `/dir/name`.\n\n- **`.html` URLs** become one exact redirect each (a pattern can't strip a suffix). Never add one from `/dir/`, `/dir/index.html`, or `/index.html` (SKILL.md, Redirects).\n- **`redirect_from`** values become redirects as written (`/old` and `/old/` are different `from`s), keeping case (`/w/Main_Page`); one that is the page's own URL is skipped. **`redirect_to`** becomes a redirect from the page's route, and an empty page with it is deleted.\n- **Raw HTML** (`<a href>`, `<img src>`, `<video><source src>`) gains `deployment.base` on a root path, as a Markdown link does, so the root paths the codemod writes from relative links, `{{ site.baseurl }}`, `relative_url`, and `{% link %}` need nothing more. `blume validate` checks a raw `<a href>` against the routes and a media `src` against `public/`.\n- **Jekyll 3's `{% link %}`** doesn't add `baseurl` (Jekyll 4's does, so `{{ site.baseurl }}{% link … %}` doubled it there). The codemod drops both: Blume adds the base itself.\n\n## Content: what the codemod leaves\n\n**HTML includes** (`{% include x.html … %}`) are never spliced: `<include>` embeds a non-Markdown file as code. Replace each use and delete the partial when nothing uses it:\n\n- a figure → a Markdown image, or `<Frame caption=\"…\">` around it;\n- a YouTube iframe → `<YouTube url=\"…\" />`, inside `<Frame caption>` when the include printed a caption; a `width` → drop (report). A playlist URL (`/embed/videoseries?list=…`) works as written;\n- a row of embeds or code blocks in a flex `<div>` → `<Columns cols={2}>`;\n- a jQuery UI accordion (`$(\".accordion\").accordion()` over alternating question lines and answer `<div>`s) → `<Accordion>` with one `<AccordionItem title>` per question, the first `defaultOpen` as jQuery UI opened it; an `<details>` with Markdown in it → `<Expandable title>` (MDX wraps a `<summary>` and the text after it in one paragraph);\n- a note include (`note.html`, `callout.html`) → a directive.\n\n**Liquid logic** (`{% if %}`, `{% for %}`, `{% assign %}`, `{% capture %}`, a dynamic `{% include {{ var }} %}`, a parameter passed as a variable) → the output the old build shows: split a partial with `{% if include.x %}` into one partial per variant, write a `_data` loop's output once as Markdown, inline `{{ page.x }}`. A `{{ site.x }}` that isn't a plain value in `_config.yml` is left too. Liquid ran inside fenced code as well, unless wrapped in `{% raw %}`, so a REVIEW in a code block means the old page showed the rendered value.\n\n**CSS that changed content.** The codemod lists every `display: none` and `content: \"…\"` rule in `_sass/` and `assets/css/`. Find what each targets in the pages: content a rule hid wasn't shown, so delete it (and report the anchors that go with it); text a `::before`/`::after` added (`.label::before { content: \"New: \" }`) belongs in the converted element's text.\n\n**MDX hazards** the codemod lists in `.mdx` pages: a brace in prose (escape it `\\{` or put it in backticks), an indented code block (fence it), anything else the build names with a line. A partial with a hazard fails the build at the partial's own line (`blume check` warns `BLUME_MDX_SYNTAX` there first), and an unclosed `<img>` in a partial doesn't fail at all: it pulls the rest of the partial inside its parent element. The codemod self-closes the ones it sees, and `BLUME_MDX_UNCLOSED_ELEMENT` names the partial's line for any left.\n\n**A title in a partial:** a page that's only `{% include api/x.md %}` takes its heading from the partial's H1. Move it into the page's `title` (plain text: no `<kbd>` or backticks) and out of the partial, or Blume renders two.\n\n**Also by hand:** definition lists (`Term` + `: Definition` → a list or `<dl>`), `*[ABBR]:` abbreviations (delete), Font Awesome `<i class=\"fa…\">` icons (`<Icon icon>` with a Lucide name, or drop), and page scripts the codemod removed (an island, a `PageFooter` slot, or static content: two images instead of a before/after slider).\n\n### Callouts\n\nJTD has no built-in callout names: each site declares its own under `callouts:` with a `color` and an optional `title`, which JTD printed uppercase above every callout of that name. The codemod maps by the name's meaning, then by color:\n\n| JTD name (template color) | Blume |\n| --- | --- |\n| `note` (purple), `important` (blue) | `:::note`, `:::important` (an alias of note) |\n| `tip`, `hint` | `:::tip` |\n| `info`, `highlight` (yellow, untitled) | `:::info` |\n| `warning`, `attention`; `caution` | `:::warning`; `:::caution` (an alias of warning) |\n| `danger`; `error` | `:::danger`; `:::error` (an alias of danger) |\n| `new`, `success` (green) | `:::success` |\n| any other name | by its color: blue → `info`, green → `success`, yellow → `warning`, red → `danger`, purple or grey → `note` |\n\nBlume shows a type's icon, not its name, so a configured `title` that only names the directive is dropped and one that says more is kept (`new` titled \"New\" → `:::success[New]`). `{: .note-title }` takes its title from the blockquote's first paragraph. A callout class on a list, table, or code block rendered no box in JTD (it styles only paragraphs and blockquotes), so it's dropped. The codemod applies each IAL to the block Kramdown did: a list or blockquote that follows a paragraph without a blank line is its own block, and an IAL with blank lines on both sides applies to the next block. An IAL indented under a list item sat inside the item, where Kramdown sometimes boxed the item's paragraph: those are reported, so check the old page and write the callout inside the item by hand.\n\n## Front matter\n\nThe codemod maps these, and reports every other key Blume's strict schema rejects:\n\n| Jekyll / JTD | Blume |\n| --- | --- |\n| `title` + body H1 | `title` = the H1; a different old title → `sidebar.label` and `seo.title` |\n| `description`, or `summary` (JTD showed it in child lists) | `description`. JTD only put it in `<meta>`; Blume shows it under the title (report) |\n| `layout: default` / `page` / `home` / `post` / `about` | removed |\n| `layout: minimal`, `nav_enabled: false` | `mode: center` |\n| a custom `layout` from `_layouts/` | reported: pick a `mode` or a custom `.astro` page |\n| JTD nav keys, `permalink`, `redirect_from`, `redirect_to` | removed (Navigation, URLs) |\n| `search_exclude: true` | `search: { exclude: true }` |\n| `last_modified_date` | `lastModified` (ISO date) |\n| `published: false` | `draft: true` |\n| `image` (a string, or `{ path }`), `canonical_url` | `seo.image`, `seo.canonical` |\n| jekyll-seo-tag's `seo:` `type`/`name`/`links` | removed |\n| `sitemap: false` | reported: `seo: { noindex: true }` if it shouldn't be indexed, else delete |\n| `author`, `date`, `lang`, `tags`, `categories`, `excerpt`, `toc` | reported: delete (`tags` → `search.tags` if a hosted search filters on them) |\n\n## Theme, layouts, and assets\n\n- **`_sass/custom/custom.scss`** → a project-root `theme.css` with only the rules whose markup still exists (figure classes, flex rows): JTD selectors (`.main-content`, `.site-nav`, `.site-title`) match nothing in Blume. Turn hard-coded colors into Blume's tokens.\n- **Theme hook includes** (the codemod lists them): `head_custom.html` (favicons → `public/`, `<meta>` → `seo.metatags`, analytics → adapters, icon-font CSS → drop), `header_custom.html` (an announcement → `banner`), `footer_custom.html` (Config), `title.html` (→ `logo`), `nav_footer_custom.html`, `search_placeholder_custom.html`, `toc_heading_custom.html`, `mermaid_config.js`, `js/custom.js`, `lunr/*` → drop and report what each did.\n- **`_layouts/`** and `.html` pages with front matter → `.mdx` (with `mode: custom` for a landing page) or a custom `.astro` page. `404.html`/`404.md` → delete (Blume ships one).\n- **Assets:** a folder the old site served by root path (`/media/…`, `/assets/images/…`) moves whole to `public/` at the same path. Keep files no page references: other sites may link them. List them, and prune only once the user confirms. JTD's own `assets/css` and `assets/js` go.\n- **Root files** (`favicon.ico`, `apple-touch-icon.png`, `site.webmanifest`, `.well-known/`) → `public/`. A `robots.txt` replaces Blume's generated one: keep it only if it says something Blume's doesn't. `CNAME` and `.nojekyll` aren't needed with a GitHub Actions deploy.\n\n## Collections and posts\n\n- **A JTD collection** (`collections: { docs: { output: true, permalink: \"/:collection/:path/\" } }` plus `just_the_docs.collections.docs.name`): rename `_docs/` → `docs/` so the URLs stay `/docs/…`, then run the codemod; give the folder a `meta.ts` `title` from `name`. `nav_fold: true` → `collapsed: true`. A collection document without `title` took its filename as one: write it in.\n- **`_posts/`** → `type: blog` pages (`docs/advanced/blog.mdx`), with `slug` pinned to the old dated URL (`/:categories/:year/:month/:day/:title.html` by default, so also an `.html` redirect) or redirected; `{% post_url %}` → the post's route; jekyll-feed's `/feed.xml` → a redirect to `/blog/rss.xml`.\n\n## Plugins\n\n| Plugin | Blume |\n| --- | --- |\n| `jekyll-redirect-from` | `redirects` (the codemod) |\n| `jekyll-seo-tag`, `jekyll-sitemap` | built in (the sitemap needs `deployment.site`) |\n| `jekyll-relative-links` | built in: the codemod turns `.md` links into routes |\n| `jekyll-include-cache` (`include_cached`) | same as `include` |\n| `jekyll-remote-theme`, `jekyll-default-layout`, `jekyll-optional-front-matter`, `jekyll-titles-from-headings`, `jekyll-readme-index` | delete; the old build shows the pages and titles they made |\n| `jekyll-github-metadata` (`site.github.*`) | inline the values; `repository` → `github` |\n| `jekyll-last-modified-at` | `lastModified: \"git\"` |\n| `jemoji` (`:smile:`), `jekyll-mentions` (`@user`), `jekyll-gist` | paste the emoji; write the links; a link or a fence |\n| `jekyll-paginate`, `jekyll-coffeescript`, `jekyll-sass-converter`, `jekyll-compose` | drop |\n| anything else (`_plugins/*.rb` included) | read what it does; report it |\n\n## Plain Jekyll and other themes: manual\n\nRun the codemod without `--old` (or with it, for the URLs): its content and link passes apply to any Jekyll site. The navigation source differs, and there's no tested mapping for it:\n\n- **Plain Jekyll / minima:** `header_pages` (a list of files) or no nav. Lay out folders by topic and report the new sidebar.\n- **Minimal Mistakes** (docs layout): `_data/navigation.yml` holds the sidebar (a key such as `docs:` listing `title` + `children: [{ title, url }]`), and pages opt in with `sidebar: { nav: \"docs\" }`. Convert it like a config-declared nav (SKILL.md). Notices `{: .notice }` and `.notice--primary`/`--info`/`--warning`/`--success`/`--danger` → pass those names with `--callouts` and map the types by hand; `toc: true` and `toc_*` → delete.\n- **Documentation Theme for Jekyll:** `_data/sidebars/*.yml` (`entries` → `folders` → `folderitems`, then `subfolders` → `subfolderitems`), with `sidebar: <file>` and a flat `permalink: name.html` on every page. `{% include note.html content=\"…\" %}` (also `tip`, `warning`, `important`, and `callout.html` with a `type`) → directives; `summary` → `description`; `keywords` → `search.keywords`; `last_updated` → `lastModified`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts` (a Ruby site has none: scaffold it), Node 22.19+, `.gitignore` for `node_modules/`, `.blume/`, `dist/`, `.env.local`.\n- **CI:** replace `bundle exec jekyll build` and the Pages starter workflow with the Blume one (https://useblume.dev/guides/markdown-docs-github-pages). Its `npm ci` needs a lockfile, so commit the `package-lock.json` from `npm install` in the same change. A branch-deployed Pages site moves its source to GitHub Actions in the repo settings, where the custom domain also stays: list both as manual steps. Dependabot's `bundler` entry → `npm`. Scripts that wrapped Jekyll (`serve.bat`, `Rakefile`) → delete or repoint.\n- **Delete** `_config.yml`, `Gemfile`, `Gemfile.lock`, `_layouts/`, `_sass/`, theme hook and HTML includes, JTD's `assets/` files, `_site/`, `.jekyll-cache/`, and `vendor/bundle/`, once everything they declared is harvested. Keep `_includes/` partials pages still `<include>`, and `jekyll-migration.json`.\n- **Grep the whole repo** for moved page paths and for `jekyll` (CODEOWNERS, link checkers, a README \"build the docs\" section).\n\n## Leftover check\n\nRerun until it prints only code samples, `{{name}}` variables and include props you defined, and relative links you meant:\n\n```bash\ngrep -rnE '\\{:[ .#a-z:]|\\{%|\\{\\{|^\\*\\[|^: |markdown=\"|<i class=\"fa|\\.html[)#\"]|\\]\\(\\.\\.?/[^).]*\\)|\\]\\(/[^)]*/(#[^)]*)?\\)' \\\n  --include='*.md' --include='*.mdx' --exclude-dir={node_modules,vendor,_site,.blume,dist} .\n```\n\n## Dropped — report these\n\nCallout title labels that only named the type, unconfigured or malformed callout IALs left plain, the button and label styling the codemod dropped, `nav_external_links` moved above the sidebar, `back_to_top`, other `footer_content` text and links inside the copyright, theme hooks, custom color schemes beyond the accent, `_sass` rules with no Blume target, content a CSS rule hid, site-wide code line numbers, deeper TOC levels, `UA-` analytics ids, `sitemap: false`, abbreviations, definition-list styling, Liquid logic and `_data` loops now frozen as content, embed widths, page scripts and widgets (sliders, accordions) not rebuilt, icon-font icons with no Lucide match, accidentally published files (`LICENSE`, `.bat`), and every plugin removed without an equivalent.\n\n<!-- File: .agents/skills/blume-migrate/references/mdbook.md (Tokens: ~9352 | Category: anthropic-skill) -->\n# mdBook → Blume\n\nmdBook is the Rust project's book generator: a `book.toml`, a hand-written `SUMMARY.md` that is the whole table of contents, and chapters in `src/` rendered by pulldown-cmark after preprocessors rewrite them (the built-in `links` expands `{{#include}}`; plugins like mdbook-admonish and mdbook-mermaid add the rest). Watch for five traps:\n\n- **Every URL ends in `.html`** (`src/guide/setup.md` → `/guide/setup.html`, a `README.md` → `index.html`), so every chapter that isn't an index needs an exact redirect.\n- **`SUMMARY.md` is the only navigation, and its nesting rarely exists on disk.** A parent chapter is a page _and_ a group, and its file sits beside its children's folder (`guide.md` + `guide/`) or flat next to them.\n- **`{{#include}}` pulls code from anywhere in the repo, by `ANCHOR` name or line range.** Blume's `<include>` takes whole files from inside the content root only, so code the crate compiles goes through generated excerpts (Includes). Anchors that no longer exist rendered as empty blocks on the old site, with no warning, so expect several.\n- **Fences carry Rust-only syntax.** Hidden `# ` lines and attributes like `rust,ignore` show up as code or kill highlighting if left alone.\n- **The page heading is the body H1, but the sidebar label and `<title>` are the `SUMMARY.md` text**, and the two often differ.\n\nScope: mdBook 0.4.x and 0.5.x, checked against 0.4.48, 0.4.52, and 0.5.4, mdbook-admonish 1.19.0, mdbook-mermaid 0.15.0, and an end-to-end migration of the Linera manual (39 chapters, mdBook 0.4.48). The syntax of mdbook-katex, -toc, -tabs, and -alerts comes from their docs; it wasn't run.\n\n## Detect\n\n- **`book.toml`**, with `[book]` and usually `[output.html]` tables: only mdBook uses it. It sits at a standalone book's root or in a folder of a code repo (`docs/`, `book/`, `doc/`, `guide/`, `mdbook/`). The chapters live in `[book] src` (default `src/`), which can point outside that folder (`src = \"../docs\"`).\n- **`SUMMARY.md`** in that `src`. GitBook and HonKit use `SUMMARY.md` too, but with `.gitbook.yaml` or `book.json`, never `book.toml`.\n- CI that runs `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, or `cargo install mdbook mdbook-<plugin>`. The workflow that deploys may live in another repo (one that checks the code repo out as a submodule).\n- **Fingerprint the version** from the CI pin. 0.5 removed `curly-quotes`, `multilingual`, `google-analytics`, and `copy-fonts`, rejects unknown `book.toml` keys, and fails on plugins built for 0.4 (mdbook-admonish 1.19.0 and mdbook-mermaid 0.15.0 both do), so most books with plugins still build with 0.4.x.\n\n**Where the Blume project goes.** Beside `book.toml`, the folder `mdbook build` ran in, with `content.root` set to the book's `src` (Blume's default is `docs`, so set it unless `src = \"docs\"`). That keeps `package.json`, `public/`, and `dist/` out of a Rust repo's root. mdBook has no manifest: scaffold `package.json` there (SKILL.md step 6). **A `src` outside that folder (`\"../docs\"`) works**, edit links included, and a `meta.ts` there imports `defineMeta` from `blume` as usual. That import needs a Blume release after 2.1.3: earlier ones resolved `blume` only from the meta file's own folder up, so it failed with `BLUME_META_LOAD_FAILED … Cannot find module 'blume'`. On those, export a plain object instead (`export default { title: \"Developers\", display: \"flat\", pages: […] };`).\n\n## Build the old book first\n\nBefore changing anything, build the book with the version CI pins and its plugins, into a folder outside the repo. Prebuilt binaries of mdBook and most plugins are on their GitHub releases pages (mdbook-admonish has none for Apple Silicon: `cargo install --root <scratch> mdbook-admonish@<version>`); install them in a scratch folder, not globally.\n\n```bash\nmdbook build <book-root> -d /abs/path/old-book\nMDBOOK_OUTPUT='{\"markdown\": {}}' mdbook build <book-root> -d /abs/path/old-md\n(cd /abs/path/old-book && find . -name '*.html' ! -name print.html ! -name toc.html ! -name 404.html | sed 's|^\\.||' | sort) > old-urls.txt\n```\n\n- `old-book` is the old site: its `.html` files are the old URLs, its headings carry the old ids, and its pages show what readers saw. With two or more `[output.*]` tables (`[output.linkcheck]` counts), the HTML lands in `old-book/html/`: use that folder wherever this file says `old-book`.\n- `old-md` is each chapter as Markdown after `{{#include}}` and the other helpers ran (admonish blocks stay as written): what every include actually showed.\n- Under 0.4 a missing plugin only warns, and its blocks render as code; the URLs are still right. Under 0.5 it fails unless `optional = true`.\n\n## Config: `book.toml` → `blume.config.ts`\n\nMap only what's set. CI can override keys with `MDBOOK_BOOK__…`/`MDBOOK_OUTPUT__…` environment variables, so read those too.\n\n| `book.toml` | Blume |\n| --- | --- |\n| `[book] title` | `title`. Unset, mdBook showed no title: take the first chapter's H1, or ask |\n| `description` | `description` |\n| `src` | `content.root` |\n| `language` (not `en`) | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`); `text-direction = \"rtl\"` → that locale's `dir: \"rtl\"` |\n| `authors`, `multilingual`, `[rust] edition`, `[build] build-dir`, `create-missing`, `extra-watch-dirs` | drop. With `create-missing` (on by default) mdBook created an empty page for a `SUMMARY.md` entry whose file was missing: check the old build |\n| `use-default-preprocessors = false` | without `[preprocessor.links]`/`[preprocessor.index]`, `{{#…}}` stayed literal and `README.md` stayed `README.html`: read the old build |\n| `[output.html] site-url = \"/repo/\"` | `deployment: { base: \"/repo\" }`. It's a path, not an origin |\n| `cname` | `deployment: { site: \"https://<cname>\" }`; GitHub Pages isn't auto-detected. A project site with no domain → `{ site: \"https://<owner>.github.io\", base: \"/<repo>\" }` |\n| `git-repository-url` on GitHub | `github: { owner, repo }`. Elsewhere → `navigation.actions` (report) |\n| `edit-url-template` | `github.branch` from the segment after `edit/` or `blob/`. Set `github.dir` to the Blume project's path in the repo (`git rev-parse --show-prefix`), not the template's prefix: mdBook fills `{path}` with `<src>/<file>`, so a `src` outside the book root gave the old site broken edit links (`…/edit/main/../docs/x.md`) |\n| `default-theme`, `preferred-dark-theme` | mdBook switches between them on the reader's system preference. Both dark (`coal`, `navy`, `ayu`) → `theme.mode: \"dark\"`; a light one plus a dark one → omit. A theme switcher deleted from `theme/index.hbs`, or a stylesheet that repaints the default theme, forces one look: read the CSS, not the name |\n| `additional-css` | a root `theme.css` with only the rules whose markup still exists (`.content`, `.sidebar`, `.chapter`, `--links`, `--sidebar-*` match nothing). Read brand colors and fonts into `theme.accent`, `theme.background`, and `theme.fonts`; Blume already defaults to Inter and IBM Plex Mono. Delete `mdbook-admonish.css` |\n| `additional-js` | delete the mermaid, admonish, and KaTeX assets. A gtag loader → `googleAnalytics({ id })` from `blume/analytics`; other scripts → `script()` or report |\n| a `theme/` folder | diff it against `mdbook init --theme` from the same version first: stock files carry nothing to port, and the stock `favicon.png`/`favicon.svg` are mdBook's own logo (report, don't copy). Custom ones: `<head>` meta tags → `seo.metatags`; analytics → adapters; a favicon → `public/`; fonts → `theme.fonts`; the rest drops (report) |\n| `google-analytics` (0.4) | `googleAnalytics({ id })` for a `G-` id; report a `UA-` id |\n| `mathjax-support` | Math (Other syntax) |\n| `smart-punctuation` / `curly-quotes` | Blume always converts quotes and dashes. Where the old site had it off (0.4's default), `--flag` in prose becomes an en dash: put flags in backticks |\n| `no-section-label` | nothing. Without it, mdBook numbered the chapters (`1.`, `2.3.`) in the sidebar, and Blume can't: report it, and fix prose that cites \"chapter 4\" |\n| `input-404`, `src/404.md` | delete the page; Blume ships a 404 (`pages/404.astro` to customize) |\n| `[output.html.search]` | built in: drop. `search.chapter.\"<path>\" = { enable = false }` → `search: { exclude: true }` on those pages |\n| `[output.html.fold]` | Navigation |\n| `[output.html.redirect]` | URLs and redirects |\n| `[output.html.playground]`, `[output.html.code.hidelines]` | Code blocks |\n| `[output.html.print]`, `hash-files`, `copy-fonts`, `sidebar-header-nav`, `git-repository-icon`, `definition-lists`, `admonitions`, `live-reload-endpoint`, `[output.markdown]` | drop |\n| `[output.linkcheck]`, `[output.linkcheck2]` | `blume validate --strict` (`--external` for web links) |\n| other backends (EPUB, PDF) | `export: { pdf: true }` prints one page at a time; a whole-book PDF or EPUB has no equivalent (report) |\n\n## Preprocessors\n\n| `[preprocessor.X]` | Blume |\n| --- | --- |\n| `links` (built in) | Includes |\n| `index` (built in) | rename every `README.md` chapter to `index.md` |\n| `admonish` | Admonitions |\n| `mermaid` | ` ```mermaid ` works as written in `.mdx`; delete `mermaid.min.js` and `mermaid-init.js` (theme settings in `mermaid-init.js` drop) |\n| `katex` | `$…$` → `$$…$$` inside its sentence; `$$` blocks stay; `\\$` → `$`. Custom delimiters → convert from those. A `macros` file has no equivalent: expand the macros |\n| `toc` | delete the `<!-- toc -->` marker (or the configured `marker`): the outline rail replaces it |\n| `pagetoc`, `open-on-gh` | built in (outline rail; `github` edit links): delete |\n| `alerts` | GitHub alerts (Admonitions) |\n| `tabs` | `{{#tabs}}` / `{{#tab name=\"X\"}}` … `{{#endtab}}` / `{{#endtabs}}` → `<Tabs>` / `<Tab title=\"X\">`. `global=\"…\"` drops: Blume syncs same-titled tabs page-wide |\n| `i18n-helpers` (gettext, `po/*.po`) | i18n (Repo integration) |\n| `cmdrun` (`<!-- cmdrun … -->`), `template`, custom ones (`command = \"cargo run …\"`) | diff `old-md/` against `src/` to see what each one wrote, then freeze that output or move the generator into a prebuild script (report) |\n| `linkcheck` | `blume validate --strict` |\n| other diagram plugins (graphviz, plantuml, svgbob) | render each diagram to an SVG once and commit it, or redraw it in mermaid (report) |\n| anything else | read what it does; report it |\n\n## Navigation: `SUMMARY.md` → folders and `meta.ts`\n\n### How mdBook reads `SUMMARY.md`\n\n- A `# Summary` first line is ignored. **Prefix chapters** are bare `[Title](file.md)` lines before the first list, and can't nest. **Part titles** are `# Title` headings (h1 only) over the numbered chapters that follow: plain headings, not links. **Numbered chapters** are list items (`-`, `*`, or `1.`) that nest by indentation. **Suffix chapters** are bare links after the last list. A **draft** is `[Title]()` (a greyed-out entry, no page). A **separator** is a `---` line; it can also split one part's list, leaving chapters after it under no visible heading.\n- Only listed chapters render. Any other `.md` under `src/` isn't published, but every non-Markdown file there is copied to the site at its own path.\n- A chapter's URL is its path with `.html`; a `README.md` (any case) becomes `index.html`. **The first chapter is also written to `/index.html`.** Its sidebar label and `<title>` are the `SUMMARY.md` text (plus ` - <book title>`); the page shows its own H1.\n\n### Put every chapter in place\n\nKeep each chapter's path, so its route is the old URL minus `.html`, and keep a table of old URL → file → route as you go: it becomes `redirects`.\n\n- **Every `README.md` chapter → `index.md`.** Its route is the folder's, which `x/index.html` already served. Left as is, Blume publishes it at `/x/README`.\n- **A parent chapter beside its children's folder** (`guide.md` + `guide/`): `git mv guide.md guide/index.md`. The route stays `/guide` and the folder row links to it; left in place, `guide.md` is a separate page beside an unlinked \"Guide\" group. The page moved a level deeper: rewrite its relative links, images, and include paths, and every link to `guide.md`.\n- **Children flat beside their parent** (`ch03-00-concepts.md`, `ch03-01-variables.md`): a `(group)/` folder holding the parent as `index.md` with `slug: ch03-00-concepts`, plus the children. The group adds no URL segment, the `slug` keeps the parent's route, and the group row links to it. Rebase relative paths in every moved file.\n- **The first chapter**, when it isn't the root `README.md`: move it to the content root's `index.md` and redirect its own URL to `/` (`{ from: \"/introduction.html\", to: \"/\" }`), since mdBook served it at both.\n- **Ordering prefixes.** Blume drops a leading `\\d+[-_.]` from every segment, keeping versions (`1.2.0`) and dates (`2024-01-05`, `12-05-2022`, `2024-01`) whole (`00-overview.md` → `/overview`). Every old URL is redirected anyway, so let Blume drop them (they keep ordering the sidebar) and point those redirects at the new routes, or pin `slug` on each page to keep the old path. Say which.\n- **Files `SUMMARY.md` doesn't list** were never published: a contributors' `README.md` beside the chapters, notes, Markdown that `{{#include}}` pulls in. Prefix them with `_`, move them out of the content root, or add them to `content.exclude` (it adds to the defaults `\"**/_*\"` and `\"**/.*\"`). Report them.\n- **A chapter whose body is a `<meta http-equiv=\"refresh\" content=\"0; url=…\">`** is a redirect, not a page to keep: add a redirect from its `.html` URL to the target, a `navigation.featured` link in place of its sidebar entry, and delete it (mdBook's `print.html` ran the same tag, so it sent the whole print page away too). Report it.\n- Delete `SUMMARY.md` once folders and `meta.ts` carry its structure: it would publish as a page.\n- **Check that git sees the new files.** A repo `.gitignore` can cover the content root (pages added before the rule stay tracked), and then every `meta.ts` and partial you create is silently left out of `git add`. Run `git check-ignore -v <content root>/meta.ts` after writing the first one; if it prints a rule, fix that rule or report that each new file needs `git add -f`.\n\n### Parts, order, labels, and folding\n\n- **Display:** set `navigation: { sidebar: { display: \"group\" } }`. Parent chapters become collapsible rows, like mdBook's tree, and a group keeps leaf chapters between its subgroups in `meta.ts` order (SKILL.md: `flat` can't interleave them).\n- **Part titles** → a folder per part, with `title: \"<part title>\"` and `display: \"flat\"` in its `meta.ts`: a heading, not a row. When a part's chapters already share a folder (`developers/`), that folder is the part; otherwise use a `(part)/` folder at the content root, which changes no URL. Chapters after a separator inside a part get the folder they're already in (`appendix/` reads \"Appendix\"), or the part's heading.\n- **Order** → a `meta.ts` `pages` array in each folder, every child in `SUMMARY.md` order. The root `meta.ts` orders the parts.\n- **Prefix chapters** stay loose pages at the content root, which Blume lists first, as mdBook did.\n- **What files can't express:** at the top of the sidebar, loose pages always list above folders. Suffix chapters, and leaf chapters between parent chapters in a book with no parts, rise above them. Put each in a `(group)/` folder (it adds a heading), or accept the order. When many chapters are affected, an explicit `navigation.sidebar` keeps `SUMMARY.md`'s order exactly: a route string per leaf chapter, `{ label, root: \"<route>\", items }` per parent chapter, `{ label, display: \"flat\", items }` per part. It replaces the generated tree, so it needs an entry per new page, as `SUMMARY.md` did. Report which you chose.\n- **Labels:** `title` is the body H1, what the page showed. `sidebar.label` is the `SUMMARY.md` text where it differs, what the sidebar and `<title>` showed. A page with no H1 takes the `SUMMARY.md` text as `title`. Give each parent chapter's folder `title: \"<its SUMMARY.md text>\"` in `meta.ts`, or its row shows the humanized folder name. Strip Markdown from `SUMMARY.md` text: backticks render literally in `title`.\n- A parent's `index` page also lists as its group's first row, so the label shows twice. To show it once, as mdBook did, set `sidebar.hidden: true` on the index: it stays in `sitemap.xml` and search because the group row still links to it. If its `title` differs from the folder's `meta.ts` title, set its `sidebar.label` to the folder title, or `BLUME_NAV_INDEX_TITLE_MISMATCH` fails `validate --strict`.\n- **`[output.html.fold]`:** with `enable = true`, sections below `level` started closed (0 closes all), which is `group` mode's default; add `collapsed: false` to the folders mdBook opened. Without folding, mdBook showed every section open: add `collapsed: false` to every folder's `meta.ts`.\n- **Drafts and separators** have no equivalent: drop them, and report the drafts as planned chapters.\n\n## Content\n\n### `.md` or `.mdx`\n\nRename a page to `.mdx` when it ends up with a callout, mermaid, math, tabs, or another component. Those are MDX-only: in `.md` they don't render, and the build stays green (a `:::` callout only warns `BLUME_MD_DIRECTIVE`). Tables, footnotes, strikethrough, task lists, `[#id]` heading ids, `<include>`, `{{variables}}`, and raw HTML work in both (`{#id}` only in `.md`). Fix what fails `.mdx` in each renamed page: HTML comments → `{/* … */}` on one line, unclosed `<br>`/`<img>`, `<https://…>` autolinks, and `{`, `<` in prose (`Vec<T>` outside backticks).\n\n### Includes and the other `{{#…}}` helpers\n\nmdBook expands these as text before parsing, so they work inside fences, inline code, and link URLs, and across a line break (`{{#include` at a line's end, the path on the next, as Prettier wraps it). A path resolves from the including file.\n\n| mdBook | Blume |\n| --- | --- |\n| a fence holding only `{{#include path}}`, the whole file, inside the content root | the whole fence → `<include lang=\"rust\">path</include>` on its own line (the fence's language; its attributes drop) |\n| a fence holding only `{{#include path:anchor}}`, `path:N`, `path:N:M`, `path:N:`, or `path::M`, or any target outside the content root | an excerpt (below), included the same way |\n| a fence mixing includes with hand-written lines | one excerpt that joins `{ \"text\": \"…\" }` parts and selections in order, so the block stays generated |\n| `{{#rustdoc_include path:sel}}` | an excerpt of `path:sel`: the selected lines only. The rest of the file sat behind mdBook's expand toggle, which has no equivalent (report) |\n| `{{#playground path attrs}}` | `<include lang=\"rust\">` of the file (an excerpt when it's outside the root); no Run button (report) |\n| `{{#include x.md}}` on its own line | `<include>/_snippets/x.md</include>` (a leading `/` is the content root): a partial must sit under a `_` folder or name, or it publishes as a page. Images in a partial resolve from the partial's own folder (mdBook resolved them from the including chapter), so fix their paths |\n| a one-line file included inline (`{{#include ../VERSION}}` in a URL or a command) | `{{VERSION}}`, with `variables: { VERSION: readFileSync(new URL(\"../VERSION\", import.meta.url), \"utf8\").trim() }` in `blume.config.ts` (`import { readFileSync } from \"node:fs\"`). Variables are replaced in links, code, and inline code too |\n| `{{#title X}}` | `seo.title: X` (Blume appends the site title, as for every page) |\n| `\\{{#include x}}` (often in a fence that documents mdBook) | drop the backslash, which mdBook removed even inside code. `{{#include x}}` stays literal in code and in `.md`; in `.mdx` prose a `{` fails the build, so put it in inline code |\n\n**Excerpts.** `<include>` fails the build on a target outside the content root (`BLUME_INCLUDE_OUTSIDE_ROOT`) and can't select lines. Pasting each excerpt into its page would freeze code that cargo still compiles and tests, so generate them before every run with the bundled `include-excerpts.mjs`:\n\n1. Copy `<skill>/scripts/include-excerpts.mjs` beside `blume.config.ts`. Its header documents the manifest.\n2. Write `excerpts.json` there: `out` (`<content root>/_excerpts`, relative to the manifest; Blume doesn't publish a `_` folder), and one entry per excerpt, keyed by its file name under `out`. An include's argument works as written once its path is relative to the manifest's folder (`\"counter/abi.rs\": \"../examples/counter/src/lib.rs:contract_abi\"`): the script selects anchors and line ranges exactly as mdBook did. A mixed fence is an array: `[{ \"text\": \"impl Contract for Token {\" }, \"../src/contract.rs:transfer\", { \"text\": \"}\" }]`.\n3. Run `node include-excerpts.mjs`. It checks every entry first and lists **every** missing file, missing anchor, and line range past the end of its file in one pass, then writes nothing. A clean run writes the excerpts and deletes the ones it wrote earlier that the manifest dropped (never a file it didn't write).\n4. Replace each include with `<include lang=\"rust\">/_excerpts/counter/abi.rs</include>`.\n5. Run it first in `package.json` (`\"dev\": \"node include-excerpts.mjs && blume dev\"`, and the same for `build` and a `validate` script), and gitignore `<content root>/_excerpts/`. A host's build command must be `npm run build`, not `blume build`. Rerun it after editing a source file: `blume dev` doesn't watch them.\n\n**Missing anchors.** For each anchor the script reports (the old site showed an empty block, or only a mixed block's hand-written lines), find where the code went (`git log -S \"ANCHOR: <name>\"`, or search the crate for the item), then:\n\n- **When the team owns the source, re-add the `// ANCHOR: <name>` and `// ANCHOR_END: <name>` comments around the code there.** The entry keeps working as written, and the excerpt follows the code as it changes.\n- Otherwise, point the entry at a line range (`\"…/lib.rs:14:23\"`). It keeps selecting those line numbers when the file changes, without an error, so say so in the report.\n- Drop the block, with its lead-in sentence, when the code is gone.\n\nReport each one and what you chose. Code that now fills a formerly empty block may contradict the prose around it: list what you notice.\n\n### Code blocks\n\n| mdBook | Blume |\n| --- | --- |\n| ` ```rust,ignore `, `rust,no_run,noplayground`, `rust,should_panic`, `rust,compile_fail`, `rust,edition2021`, `rust,editable`, `rust,mdbook-runnable`, `rust,hidelines=…` | ` ```rust ` (a comma-glued language doesn't highlight, and warns `BLUME_UNKNOWN_CODE_LANGUAGE`; written with a space, ` ```rust ignore `, the keyword does nothing and warns `BLUME_CODE_FENCE_OPTION`) |\n| ` ```ignore `, or no language: rustdoc tests an unlabeled block as Rust, so books label diagrams and shell lines `ignore` | ` ```text `, or the real language |\n| **hidden lines** in a Rust block: first non-space character `#`, then a space or the end of the line | delete the line, and a blank line it leaves at the top of the block: readers saw it only through the eye toggle, which has no equivalent. `##x` shows as `#x`; `#[derive]` and `#!` lines are code. Lines with a `[output.html.code.hidelines]` prefix (`python = \"~\"`), or a fence's `hidelines=<prefix>`, go the same way |\n| the play button, `editable` | static code (report) |\n| `mdbook test` | nothing compiles Blume's code blocks. Excerpts stay tested by cargo; hand-written blocks that `mdbook test` compiled aren't any more (report; drop the CI step and whatever it built only for that) |\n\n### Admonitions\n\n**mdbook-admonish** writes ` ```admonish <type> ` with options (`title=\"…\"`, `collapsible=true`, `id`, `class`), the older `admonish <type> \"Title\"`, or `~~~`/four-backtick outer fences around inner code. Each becomes a callout in `.mdx`, with a longer `::::` outer fence around a nested one:\n\n| admonish type (aliases) | Blume |\n| --- | --- |\n| `note`, `example` | `:::note` |\n| `info` (`todo`), `abstract` (`summary`, `tldr`), `question` (`help`, `faq`) | `:::info` |\n| `tip` (`hint`, `important`) | `:::tip` |\n| `success` (`check`, `done`) | `:::success` |\n| `warning` (`caution`, `attention`) | `:::warning` |\n| `danger` (`error`), `failure` (`fail`, `missing`), `bug` | `:::danger` |\n| `quote` (`cite`) | a blockquote |\n| **any other word** (`admonish warn`) | `:::note`: admonish rendered it as a note titled \"Note\". Don't keep the word: Blume reads `:::warn` as a warning. Report it, with the type the author probably meant |\n\n- `title=\"T\"` → `:::type[T]`; `title=\"\"` → no title. An untitled block showed the word it was written with as its title bar (`Example`, `Hint`, `TL;DR`), and an untitled Blume callout shows none: where that word isn't the Blume type's name, keep it as the title (`:::note[Example]`).\n- `collapsible=true` → `<Expandable title=\"T\">` (the type's name when untitled); its color drops.\n- Custom directives (`[[preprocessor.admonish.custom]]`) → the closest callout, with the label as title.\n- Each block had an anchor (`#admonition-<title>`, `-1` for repeats). Links to one fail `blume validate --strict`: point them at the nearest heading or drop the fragment.\n\n**GitHub alerts** (`> [!NOTE]`, built into 0.5 and the `alerts` plugin) render as callouts in `.mdx` as written: `NOTE` and `IMPORTANT` as a note, `TIP` a tip, `WARNING` a warning, `CAUTION` a danger callout. In `.md` they stay a blockquote with a literal `[!NOTE]` and warn `BLUME_MD_GITHUB_ALERT`: rename the page to `.mdx`. A 0.4 book without the plugin showed them as plain quotes: rendering them changes the page (report).\n\n### Other syntax\n\n| mdBook | Blume |\n| --- | --- |\n| `## Heading {#id}` | the same in `.md`; `[#id]` in `.mdx` |\n| `## Heading { #id .class }` | `{#id}` or `[#id]`; classes drop. Left as is, the spaced form stays in the heading text and makes a garbage id; `BLUME_MD_CURLY_ANCHOR` flags it only when no class follows the id (in `.mdx` it fails the build) |\n| footnotes, tables, strikethrough, task lists | unchanged |\n| definition lists (`term` then `: definition`, on by default in 0.5) | a list or table, or raw `<dl>`; Blume renders them as one paragraph |\n| MathJax (`mathjax-support`): `\\\\( … \\\\)`, `\\\\[ … \\\\]` | `$$…$$` inside the sentence, or on lines of its own for a block, in `.mdx`. Undo the Markdown escapes mdBook needed inside the math (`\\\\\\\\` for a LaTeX `\\\\`, any `\\_` or `\\*`): `$$` math is read verbatim |\n| `<i class=\"fa fa-x\"></i>` | `<Icon icon=\"<lucide>\" />` in `.mdx`, or delete (the icon map in `references/mintlify.md`) |\n| `class=\"left\"`/`\"right\"` floats; `class=\"hidden\"` | drop the class; delete hidden elements (report) |\n\n### Links and images\n\n- mdBook only rewrote `x.md` links to `x.html`. Blume resolves a relative `.md` link to the page that file publishes: keep them, and fix the ones into or out of every moved file.\n- Rewrite `.html` links (`installation.html#cargo`) to their `.md` files: Blume serves no `.html` URLs, so they'd depend on the redirects. `blume validate` reports each one left (`BLUME_BROKEN_ASSET`, with the page's route).\n- **A link to `README.md` was already dead:** mdBook wrote `README.html`, a page it never creates. Point it at the renamed `index.md`.\n- Root-relative links (`/guide/x.md`) land on the page that file publishes, read from the content root, like relative ones.\n- Images relative to the page work as written. So does a linked download (a PDF, an archive) that mdBook copied from `src/`: Blume publishes a file a relative link names with the page, at a new URL. Move it to `public/` at the same path only if its old URL is linked from elsewhere.\n\n## URLs and redirects\n\n| Source file | mdBook URL | Blume route |\n| --- | --- | --- |\n| `src/guide/setup.md` | `/guide/setup.html` | `/guide/setup` |\n| `src/guide/README.md` | `/guide/index.html` | `/guide` |\n| the first chapter, `src/intro.md` | `/intro.html` and `/index.html` | `/` once moved to `index.md` |\n| `src/01-basics.md` | `/01-basics.html` | `/basics`, or `slug: 01-basics` |\n\n- **One exact redirect per chapter that isn't an index**, `{ from: \"/guide/setup.html\", to: \"/guide/setup\" }`, built from `old-urls.txt` and your table: a pattern can't strip `.html`. Skip `/index.html` and `/x/index.html`: static hosts serve Blume's `x/index.html` there already.\n- **`[output.html.redirect]`**: each key is a path from the site root, and its value is a URL relative to the key's folder (`\"../infra/x.html\"`), root-relative, or external. Resolve the value against the key's folder, then map it like a chapter (`.html` → route, `index.html` → its folder); external URLs stay. Check each target against the old build: one that already led to a 404 → the page that replaced it, or drop it, and report.\n- **Fragment redirects** (`\"/page.html#old\" = \"other.html#new\"`, 0.4.52 and later): servers never see fragments. A heading renamed on the same page → pin the old id on it (`[#old]`); moved to another page → no server equivalent (report). A deleted page's own entry is its redirect.\n- **No equivalent, no redirect:** `print.html` (the whole book on one page, `noindex`), `toc.html` (the sidebar frame, `noindex`), and `404.html` (Blume writes its own). Mention `export: { pdf: true }` for printing.\n- **Hosts:** a static build writes each redirect as a meta-refresh page at `dist/<from>/index.html`, plus `_redirects` and `vercel.json`. On Netlify use `netlify({ output: \"static\" })` for real 301s (SKILL.md). GitHub Pages reads neither file and serves the meta-refresh page; how it answers `/x.html` there is unverified, so check a few old URLs after deploying.\n- **Heading anchors:** mdBook 0.5 ids match Blume's for most headings; 0.4 lowercases ASCII only (`## Über` → `#Über`), and both count the H1, so `# Install` then `## Install` gives `#install-1`. After the first build, run `node <skill>/scripts/pin-heading-ids.mjs --old old-book` (add `--map dist/blume-redirects.json` when routes changed, built with no host adapter; `--write` to apply), and rebuild until it reports 0.\n- **Check every old URL** after `blume build`. With no host adapter (static), from the Blume project:\n\n  ```bash\n  node -e '\n  const fs = require(\"node:fs\");\n  const moved = new Map(JSON.parse(fs.readFileSync(\"dist/blume-redirects.json\", \"utf8\")).map((r) => [r.from, r.to]));\n  const built = (to) => /^https?:/.test(to) || fs.existsSync(`dist${to.replace(/#.*/, \"\").replace(/\\/$/, \"\")}/index.html`);\n  const misses = fs.readFileSync(\"old-urls.txt\", \"utf8\").split(\"\\n\").filter(Boolean).filter((url) => {\n    const to = url.endsWith(\"/index.html\") ? url.slice(0, -\"index.html\".length) : moved.get(url);\n    return !(to && built(to));\n  });\n  console.log(misses.length ? misses.join(\"\\n\") : \"every old URL resolves\");'\n  ```\n\n  Then run `blume audit --only redirects`.\n\n## Repo integration and teardown\n\n- **`package.json`** beside `blume.config.ts`: `blume` plus the `dev`/`build`/`preview`/`validate` scripts, with the excerpt step first. Commit the lockfile (SKILL.md step 6).\n- **`.gitignore`:** replace `book/` with `dist/`, `.blume/`, `node_modules/`, `.env.local`, and `<content root>/_excerpts/`.\n- **CI:** replace `mdbook build`/`mdbook test`, `peaceiris/actions-mdbook`, and the plugin installs with Node 22, an install, `npm run build`, and `npm run validate`. A deploy workflow in another repo needs the same change: list it as a follow-up. For GitHub Pages, follow https://useblume.dev/guides/markdown-docs-github-pages (Actions as the Pages source), with `npm run validate` and `npm run build` in place of its `blume` steps so the excerpts exist, run from the Blume project's folder, and that folder's `dist` uploaded. A deploy that pushes a branch must publish `.nojekyll`, or Jekyll drops Blume's `_astro/` folder; `peaceiris/actions-gh-pages` adds it by default.\n- **`cargo doc` output published beside the book** (`/api/…`): keep building it in CI and copy `target/doc` into the build output, or link to docs.rs. Blume doesn't generate it.\n- **Several builds of the book** (a stable tag and `main`, `MDBOOK_BOOK__LANGUAGE` per language): migrate the current one; older versions → `versions`, or stay on the old host (report). Translations in `po/*.po` (mdbook-i18n-helpers) apply at build time: render each language's Markdown with `MDBOOK_BOOK__LANGUAGE=<xx>` and the Markdown renderer into `<xx>/`, then set `i18n`. Report it.\n- **Prettier** on the book: extend its `*.md` overrides to `*.mdx`, and keep `{/* */}` comments on one line. Prettier 3 skips what `.gitignore` ignores, so a `.gitignore` that covers the content root also makes a `prettier --check` CI step check nothing.\n- **A hand-written `llms.txt` in `src/`:** Blume generates `/llms.txt`, so delete the file. Its intro goes to `agents.llmsTxt.details`. A per-page summary becomes that page's `description` only where it reads well as the page's lede, since Blume shows `description` under the title; report the rest, and the sections that list things other than pages.\n- **Delete** `book.toml`, `SUMMARY.md`, `theme/`, the `additional-css`/`additional-js` files once ported, `mdbook-admonish.css`, `mermaid.min.js`, `mermaid-init.js`, and the `book/` folder. Grep the repo for `mdbook`, `book/`, and the old `.html` URLs (READMEs, CONTRIBUTING, examples, bots).\n\n## Dropped — report these\n\nSection numbers, separators, and draft chapters; suffix chapters and leaf chapters that moved above folders; the print page; the theme switcher and themes beyond one light and one dark; search options; the play button, the editor, and runnable or editable examples; hidden lines and their toggle; `mdbook test` coverage of hand-written blocks; `rustdoc_include` context; admonish title bars, colors, icons, custom directives, and anchors; fragment redirects across pages; the old site's empty blocks from missing anchors, and what you did with each; definition-list styling; Font Awesome icons with no Lucide match; `class=\"hidden\"` content; plugin output you froze (cmdrun, templates, custom preprocessors); a hand-written `llms.txt`; whole-book PDF or EPUB output; and every plugin you removed without an equivalent.\n\n<!-- File: .agents/skills/blume-migrate/references/mintlify.md (Tokens: ~8430 | Category: anthropic-skill) -->\n# Mintlify → Blume\n\nThe deepest migration. Mintlify declares its **entire navigation in `docs.json`**; Blume derives navigation from the filesystem. The bulk of the work is reshaping content into folders + tabs and translating config, callouts, and icons.\n\n## Detect\n\n- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.\n- A `mintlify` dependency / `mintlify dev` script.\n- Content is `.mdx` under the repo root (Mintlify has no `content.root` — pages live at the top level), with a `/snippets` folder and image dirs like `/images`.\n\nRead `docs.json` first; it drives everything.\n\n## Config: `docs.json`/`mint.json` → `blume.config.ts`\n\nResolve `$ref` includes first (Mintlify splits config across files). Map only what's set:\n\n| Mintlify | Blume | Notes |\n| --- | --- | --- |\n| `name` / `title` | `title` |  |\n| `description` | `description` |  |\n| `logo` (string or `{ light, dark, href }`) | `logo` (string, or `{ image: { light, dark, alt }, text, href }`) | Mintlify's `light`/`dark` nest under Blume's `image`; ensure files land in `public/`. **If the SVG is local and monochrome** (a solid black or white mark), rewrite its `fill`/`stroke` to `currentColor` and collapse `{ light, dark }` to the string shorthand `logo: \"/logo.svg\"` — one file that inherits the theme text color and flips with light/dark automatically. If the logo is a **wordmark** (brand name baked in), set `text: \"\"` so it doesn't render twice beside `title` |\n| `favicon` (string **or `{ light, dark }`**) | **drop the field** — copy the file(s) into `public/` under conventional names (`favicon.svg`/`icon.png`) | Blume auto-detects by filename; there is **no** favicon config field. A `{ light, dark }` source maps to a filename pair: light file → `public/icon.png`, dark file → its `-dark` sibling `public/icon-dark.png` (same directory and extension — convert if the formats differ) |\n| `colors.primary` + `colors.light` | `theme.accent` (`{ light, dark }`) | accent is now **per-mode**: `accent: { light: <colors.primary>, dark: <colors.light> }`. If Mintlify sets only `primary`, collapse to the string shorthand `accent: \"<colors.primary>\"` (applies to both modes) |\n| `colors.dark` | `theme.action` |  |\n| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) |  |\n| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |\n| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |\n| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |\n| `background.decoration` | **drop** | no Blume equivalent |\n| `fonts.family` / `fonts.{heading,body}.family` | `theme.fonts.{display,body}` | a curated slug (kebab-case, e.g. `space-grotesk`) maps directly; any other Google family maps to the object form `{ name: \"Family Name\" }`; a self-hosted font (`fonts.*.src` URLs) maps to `{ name, variants: [{ src, weight }] }` after downloading the files into the project |\n| `banner` | `banner` (`{ content, dismissible, id, link: { href, text } }`) | **only** those keys — drop `banner.color`/`banner.type` |\n| `styling.latex: true` | **drop the field** — block math `$$…$$` renders in `.mdx` with no config | there is **no** `markdown.math` field; Blume writes inline math with two dollar signs too — convert each inline `$…$` to `$$…$$` inside the sentence, checking every pair is math and not currency or a shell variable |\n| `styling.codeblocks.theme` | `markdown.code.theme` (`{ light, dark }`) |  |\n| `search.prompt` | **drop** | no equivalent |\n| `seo.metatags` | `seo.metatags`, minus the tags Blume writes itself | keep verification tokens, `theme-color`, and the like as written; Blume refuses `description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, and `article:*` dates, so move those to their settings (`og:image` → `seo.og` or a page's `seo.image`, `twitter:site` → `seo.x.handle`) and drop the rest. `seo.paths` (folder tags) has no equivalent: set per-page `seo` frontmatter |\n| `seo.indexing: \"all\"` | `search.indexing.includeHiddenPages: true` |  |\n| `variables` (`{{name}}`) | `variables` | same syntax and names (letters, digits, `_`, `-`); pages keep their `{{name}}` references unchanged |\n| `integrations.posthog` (`{ apiKey, apiHost, sessionRecording }`) | `posthog({ key, host })` in the `analytics` list | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`); `sessionRecording: false` → `disable_session_recording: true`; every adapter is imported from `blume/analytics` |\n| `integrations.ga4` (`{ measurementId }`) | `googleAnalytics({ id })` |  |\n| `integrations.gtm` (`{ tagId }`) | `googleTagManager({ id })` |  |\n| `integrations.plausible` (`{ domain, server }`) | `plausible({ domain, host })` | `server` is a bare hostname; `host` is an origin — prepend `https://` |\n| `integrations.fathom` (`{ siteId }`) | `fathom({ site })` |  |\n| `integrations.pirsch` (`{ id }`) | `pirsch({ code })` |  |\n| `integrations.mixpanel` (`{ projectToken, region }`) | `mixpanel({ token, region })` |  |\n| `integrations.amplitude` (`{ apiKey }`) | `amplitude({ key })` |  |\n| `integrations.segment` (`{ key, cdnUrl }`) | `segment({ key, cdn })` |  |\n| `integrations.hightouch` (`{ writeKey, apiHost }`) | `hightouch({ key, host })` |  |\n| `integrations.heap` (`{ appId }`) | `heap({ id })` |  |\n| `integrations.hotjar` (`{ hjid, hjsv }`) | `hotjar({ id, version })` | both numbers |\n| `integrations.clarity` (`{ projectId }`) | `clarity({ id })` |  |\n| `integrations.logrocket` (`{ apiKey }`) | `logrocket({ id })` |  |\n| `integrations.clearbit` (`{ publicApiKey }`) | `clearbit({ key })` |  |\n| `integrations.adobe` (`{ launchUrl }`) | `adobe({ url })` |  |\n| any other `integrations` script | `script({ src, strategy, attributes })` / `vercel()` in the `analytics` list | one `script()` adapter per provider without a factory |\n| `telemetry` (`{ enabled: false }`) | `feedback: false` | Mintlify's switch also turned off its feedback widgets; Blume sends no telemetry of its own, so only the rating needs turning off |\n| Feedback toggles in the Mintlify dashboard (not in `docs.json`) | `feedback: { comments: true }` for contextual (written) feedback | ask whether the site collected written feedback; Blume sends comments through the `analytics` adapters as `feedback_comment` events. Code-snippet feedback, edit suggestions, and raise-issue have no equivalent: report them |\n| `integrations.cookies` (`{ key, value }`), or a content-folder `.js` file that injects Osano or Ethyca/Fides | `consent`, imported from `blume/consent`: `osano({ customerId, configId })` (both IDs from the `cmp.osano.com/<customerId>/<configId>/osano.js` URL), `ethyca({ privacyCenter })` (the origin that serves `fides.js`), or `native()` for Blume's own banner | Mintlify's `cookies` pair only switched off its own telemetry for readers whose localStorage lacked the value; under `consent`, every `analytics` adapter waits for the reader instead. Delete the injector script. Another consent manager (Transcend, OneTrust, …) has no adapter: report it |\n| A root `skill.md`, or `.mintlify/skills/<name>/SKILL.md` | move each into a `skills/<name>/` folder and set `agents.skills: \"./skills\"`; drop Mintlify-only frontmatter (`groups`) | Blume already generates a skill for the site at `/skill.md` (`agents.skillMd`, on when `deployment.site` is set); a root `skill.md` replaces it only if its `name` matches the site's title slug (`Acme Docs` → `acme-docs`), so set that name |\n| `contextual` (`[\"copy\",\"chatgpt\",\"claude\",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `agents.mcp.enabled` + server output (report as a follow-up) |\n| `footer` (`{ socials, links }`) | `footer: { socials, links }` | `socials` keeps its keys, except `twitter`/`x-twitter` → `x` and `earth-americas` → `website`; a key outside Blume's list drops (report); `links` is one row of `{ label, href }`: every column's `items` flatten into it in order, and each column's `header` drops (report) |\n| `api.mdx` (`{ server, auth: { method, name } }`) | `api: { server, auth: { method, name } }` | the defaults for `api`-frontmatter pages |\n| `api.playground` (`{ display, proxy }`) | `api: { playground }` | `display: \"none\"` → `playground: false`; `\"simple\"` has no site-wide form, so set `playground: simple` in each page's frontmatter; `proxy` (on by default in Mintlify) → `playground: { proxy: true }`, which needs a server-output adapter, or leave it off to send directly |\n| `redirects` (`{ source, destination, permanent }`) | `redirects: [{ from, to, status }]` | `source` → `from`, `destination` → `to` as written, patterns included (`/beta/:slug*`, `/old/article-*`); `permanent: false` → `status: 307`, otherwise `status: 308` |\n| `navigation.languages` | `i18n` | see i18n below |\n\nA minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.\n\n## Icons: FontAwesome → Lucide (required)\n\nMintlify defaults to **FontAwesome**; Blume is **Lucide-only**. Convert every icon reference — frontmatter `icon`, nav-group `icon`, `<Icon>`, `<Card icon>` — to the closest Lucide name. Discard Mintlify's `iconType` (solid/regular/brands) entirely.\n\n**Automate the frontmatter pass — don't hand-edit it.** `scripts/mintlify-codemod.mjs` (in this skill, zero-dependency) remaps every **frontmatter** `icon:` using the table below, drops icons with no Lucide equivalent, and — in the same pass — drops/renames unsupported frontmatter keys (see [Frontmatter](#frontmatter)). It's deterministic and idempotent, and reports what it changed per file plus what it couldn't (unknown icons, OpenAPI-stub flags):\n\n```bash\n# <skill> = this skill's directory (the one containing SKILL.md); dry run first (report only), then apply:\nnode <skill>/scripts/mintlify-codemod.mjs <content-dir>\nnode <skill>/scripts/mintlify-codemod.mjs --write <content-dir>\n```\n\nThe codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon=\"…\">`, `<Card icon=\"…\">`, nav-group icons in `docs.json`) it does not see — convert those by hand using the same table. Common mappings:\n\n| FontAwesome | Lucide |  | FontAwesome | Lucide |\n| --- | --- | --- | --- | --- |\n| `bolt` | `zap` |  | `gear`/`cog` | `settings` |\n| `circle-info`/`info` | `info` |  | `wand-magic-sparkles`/`magic` | `sparkles` |\n| `house` | `house` |  | `magnifying-glass` | `search` |\n| `gauge`/`gauge-high` | `gauge` |  | `puzzle-piece` | `puzzle` |\n| `envelope` | `mail` |  | `file-lines` | `file-text` |\n| `pen-to-square` | `square-pen` |  | `trash-can` | `trash-2` |\n| `xmark`/`times` | `x` |  | `circle-check` | `circle-check` |\n| `triangle-exclamation` | `triangle-alert` |  | `circle-exclamation` | `circle-alert` |\n| `arrow-right-from-bracket` | `log-out` |  | `right-to-bracket` | `log-in` |\n| `location-dot`/`map-marker` | `map-pin` |  | `comments` | `messages-square` |\n| `cube` | `box` |  | `cubes`/`boxes` | `boxes` |\n| `layer-group` | `layers` |  | `diagram-project`/`sitemap` | `workflow`/`network` |\n| `chart-line` | `chart-line` |  | `chart-simple`/`chart-column` | `chart-column` |\n| `flask` | `flask-conical` |  | `robot` | `bot` |\n| `screwdriver-wrench`/`toolbox` | `wrench` |  | `circle-question`/`question` | `circle-help` |\n| `life-ring` | `life-buoy` |  | `shield-halved` | `shield` |\n| `rocket`/`book`/`book-open`/`code`/`terminal`/`key`/`lock`/`user`/`users`/`database`/`server`/`cloud`/`bell`/`calendar`/`star`/`heart`/`tag`/`folder`/`globe`/`link`/`download`/`upload`/`check`/`copy`/`play`/`filter` | _(same name — verify)_ |\n\n**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*`): Lucide still ships a few brand marks, and Blume renders them under the same name — `facebook`, `github`, `gitlab`, `instagram`, `linkedin`, `slack`, `twitter`, `youtube`. Keep those. The rest (`discord`, `x-twitter`, `docker`, `npm`, `python`, `react`…) have **no** Lucide equivalent, and FontAwesome's `apple` is the Apple logo while Lucide's `apple` is the fruit. Where no Lucide counterpart exists, **drop the icon and report it**. An icon that only marks a link to the repository or a social profile usually has a better home: the `github` config (renders the footer's repo link) or `footer.socials`. The build won't catch a wrong name for you: an unknown icon renders nothing, and it's only a warning (`BLUME_UNKNOWN_ICON`) for navigation icons — an icon prop on a component like `<Card>` fails silently — so check every name.\n\n## Navigation: `docs.json` `navigation` → filesystem + tabs\n\nMintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:\n\n- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. `directory` (`card`/`accordion`/`none`, on a group, tab, or the navigation itself) → `meta.ts` `directory` on that folder, or on the content root's `meta.ts` for the whole navigation; it inherits down the tree the same way, and lists the pages on the group's `root`/`index` page. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display, directory })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)\n- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.\n- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.\n- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `\"GET /path\"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).\n- **`tabs`** (top-level `navigation.tabs`, e.g. `App` / `API` / `API reference`) → Blume's **header tabs** `navigation.tabs` (`{ label, path, icon? }`). **Keep them as tabs — do not flatten them into `navigation.sidebar` groups or sections.** Flattening is the easy translation error: it collapses the site's top-level structure into one global sidebar and loses the header tab bar entirely. A Mintlify top-level tab is a header tab; map it to one. Put each tab's pages in **one folder** and point the tab's `path` at it — the tab then scopes the sidebar automatically (under the tab's `path`, the sidebar shows only that folder). **Blume resolves the active tab by URL prefix** (the tab whose `path` is the longest prefix of the current route), so every page in a tab must live under that tab's single `path`. Mintlify tabs freely mix pages from any folder / arbitrary routes; Blume tabs scope by prefix, so a tab that pulls in routes from all over is **not portable as-is**. You have two choices, per tab:\n  - **Move the pages under one prefix** so they share the tab's `path` (a route change → add a `redirects` entry for each moved page). This preserves the exact tab grouping.\n  - **Accept the closest shape** — assign each shared/cross-cutting page to one tab's folder and link to it from the others — when moving routes isn't worth the churn.\n\n  Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.\n\n- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.\n- **`anchors` / `navigation.global.anchors`** (persistent sidebar-top links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old \"drop and report\" for anchors.)\n- **`navbar.links`** (plain header links — Log in, Status, Support) → **`navigation.actions`** (`[{ label, href }]`), rendered in the header left of the icon buttons. A GitHub link → the `github` config (or `navigation.repo` as a URL when the docs repo is private) instead; it renders in the footer, the only place Blume links a repository. Header links hide on phones; a link that must survive there belongs in `featured`.\n- **`navbar.primary`** (the header **CTA button** — \"Get Started\"/\"Sign Up\", `type: \"button\"`) → **`navigation.cta`** (`{ label, href }`, singular: Blume renders exactly one filled button). A `type: \"github\"` primary → the `github` config instead. A route the docs don't serve (the product's own `/signup`) must be an absolute URL, or it raises `BLUME_NAV_MISSING_PAGE`.\n- **`languages`** → `i18n`, not a selector (see below).\n- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.\n\nThis reshaping **changes URLs** — a page moved from `getting-started/quickstart` into the API tab's folder becomes `/api/…`, an `index` promotion drops a segment, etc. Record each old→new path and add a `redirects` entry for it (see below); otherwise every existing link and bookmark 404s.\n\n## Content & component transforms\n\nRewrite each page's MDX:\n\n- **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type=\"x\">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. A callout with an `icon` or `color` (Mintlify's generic `<Callout icon=\"key\" color=\"#FFC107\">`) stays a `<Callout>` instead, since a directive can't set them: Blume's takes `type`, `title`, `icon` (remap it with the table below), and `color` (any CSS color); drop `iconType`.\n- **Accordions — container/item inversion!** Mintlify nests `<Accordion title=\"…\">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title=\"…\">`→`<AccordionItem title=\"…\">` (item).\n- **`<Tabs>` → `<Tabs inline>`.** Mintlify renders tabs **borderless** — a strip on a full-width rule with the content flowing beneath as prose — while Blume's `<Tabs>` defaults to a bordered box. Add `inline` to each `<Tabs>` to preserve Mintlify's appearance; every child `<Tab title=\"…\">` is unchanged. Don't add `param` — Mintlify tabs switch in place and don't deep-link to the URL, so plain `inline` is the faithful mapping.\n- **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tab>`, `<Steps>`/`<Step>`, `<View>` (its `title` becomes an option in the page's view picker, as on Mintlify; remap its `icon` with the table below and drop `iconType`). Keep them as-is.\n- **Code-block titles carry over.** Both read every bare word after the language as the title (` ```ts Acme SDK `), and `wrap` and `expandable` mean the same in both. Mintlify's other fence options don't: `lines` → `lineNumbers` (left as is, it does nothing and warns `BLUME_CODE_FENCE_OPTION`), `highlight={1-2,5}` → `{1-2,5}`, `focus={2}` → a `// [!code focus]` comment on each line, and drop `icon=\"…\"`.\n- **`<RequestExample>`/`<ResponseExample>` pass through.** Top-level ones pin to a column beside the page on wide screens, request above response, as on Mintlify; `dropdown` works the same.\n- **Hand-written endpoint pages pass through.** `api: \"POST /v1/users\"` frontmatter, with `authMethod` (`bearer`/`basic`/`key`/`none`) and `playground` (`interactive`/`simple`/`none`), works as on Mintlify: the page's `<ParamField>`s build its Try it panel and request samples. Keep them as written.\n- **API fields pass through.** `<ParamField>` (`path`/`query`/`header`/`body`, `type`, `required`, `deprecated`, `default`, `placeholder`) and `<ResponseField>` (`name`, `type`, `required`, `deprecated`, `default`, `pre`, `post`) render natively, nested `<Expandable>` fields included. Keep them as written. An older `<RequestField>` isn't supported: rewrite it as a `<ParamField>`.\n- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `githubReleases()` source.\n- **Snippets become includes.** Blume has no `/snippets` import mechanism; its reuse is `<include>`. Move each snippet into the content folder as a `_`-prefixed partial (`_snippets/x.mdx`, which never renders as a page), and replace `import X from \"/snippets/x.mdx\"` + `<X prop=\"v\" />` with `<include prop=\"v\">./_snippets/x.mdx</include>` on its own line. In the partial, rewrite each `{prop}` placeholder to `{{prop}}`: include attributes are props read with the variables syntax. Named string imports (`import { foo } from \"/snippets/vars.mdx\"`) → a site-wide `variables` entry read as `{{foo}}`, or inline the value.\n\n## Frontmatter\n\nMintlify page frontmatter → Blume's strict schema. **`scripts/mintlify-codemod.mjs --write` does the mechanical rows automatically** (rename, drop, icon-remap) and flags the judgment rows (`openapi`/`asyncapi`) for you — see [Icons](#icons-fontawesome--lucide-required) for the invocation. The table is both the mapping reference and a description of exactly what the codemod does:\n\n| Mintlify | Blume | Codemod |\n| --- | --- | --- |\n| `title` / `description` | pass through | — |\n| `sidebarTitle` | `sidebar.label` | renames |\n| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |\n| `tag` | `sidebar.badge` | renames |\n| `canonical` | `seo.canonical` | renames |\n| `og:image` | `seo.image` | renames |\n| `hidden: true` | valid top-level in Blume — **kept as-is**; add `noindex: true` yourself if the page must also leave the search index | left (do by hand) |\n| `openapi`/`asyncapi` | usually an endpoint stub → **delete the page** (Blume generates operation pages); else drop the key and keep it as a normal page | **flags** for review — never auto-deletes a page |\n| `api`, `authMethod`, `playground` | kept as written (a hand-written endpoint page, see above) | left |\n| `related` (a list) | kept as written: root-relative paths, URLs, and `{ Title: link }` entries render as Related pages cards | left; `related: true` has no equivalent (Blume has no automatic related pages), so drop it (report) |\n| `mode` | `mode`, as written | `default`, `wide`, `center`, `custom`, and `frame` carry over; `assistant` has no equivalent → drop (report) |\n| `hideFooterPagination: true` | `pagination: false` | renames (`false` drops, since the links show by default) |\n| `keywords`, `boost` | `search.keywords`, `search.boost` | renames |\n| `searchable: false` | `search.exclude: true` | renames (`searchable: true` drops, since pages are searchable by default) |\n| `public`, `rss`, `groups`, `hideApiMarker`, `iconType` | **drop** (report) | drops |\n\nThe codemod leaves the source key in place and reports a conflict rather than clobbering data when a rename target already exists (e.g. a page already has `sidebar.label`) or the value is too structured to move safely — resolve those by hand. Remove any duplicate H1 in the body — `title` renders the H1. (The codemod only edits frontmatter; it never touches the body.)\n\n## OpenAPI\n\nTop-level `openapi`, `api.openapi`, or a per-group/per-tab `openapi` → `reference: [openapi({ sources: [{ spec, label?, route? }] })]`, with `openapi` imported from `blume/reference` (`spec` alone is the single-source shorthand; several per-tab specs become several sources, or several `openapi()` entries when they need different routes; a spec the source embedded with Scalar becomes a `scalar({ spec })` entry). A Mintlify `{ source, directory }` object: `directory` → the source's `route`. An `asyncapi` field maps the same way to `asyncapi({ … })` in the list. **Delete every per-endpoint stub page** (frontmatter `openapi: \"GET /path\"`, `openapi: \"spec.json webhook name\"`, or a `\"GET /path\"` nav entry) — Blume's native renderer generates one real page per operation and per webhook, and shows callbacks on their operation's page. **Add a `navigation.tabs` entry pointing at the reference `route` yourself** (Mintlify's API tab maps to it); the reference does not create a header tab automatically.\n\n- **Vendor the spec.** Mintlify usually points at a spec **URL**. Copying that straight into `spec:` makes every build fetch it at build time — a single point of failure in CI/offline/behind a proxy, and a failed fetch silently drops the reference (leaving the tab pointing at a route that 404s). Prefer downloading it into the repo (`curl … -o openapi/<name>.json`) and pointing `spec` at that local path. If you keep the URL, report the dependency and consider a `prebuild` refresh-with-fallback.\n- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/list-models`: a camelCase tag or id is split into kebab-case, not just lowercased — see SKILL.md \"OpenAPI\") — this differs from Mintlify's endpoint URLs, so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route and flags the ones you miss.\n- **Overlays carry over.** An `openapi: { source, overlays: [...] }` object → that source's `overlays` (`openapi({ spec, overlays })`, or `overlays` on each entry in `sources`), in the same order; Blume applies Overlay 1.0 and 1.1 the same way. An overlay Mintlify found only through its `extends` field (auto-discovered, not listed in `docs.json`) must be listed explicitly: Blume doesn't scan for overlays.\n- **Code samples carry over.** `api.examples.languages` → `openapi({ codeSamples: [...] })`; Blume generates the same 18 languages and accepts Mintlify's keys and aliases (`bash`, `javascript`, `node`, `csharp`, `dotnet`, `c++`, …) as written. `api.examples.autogenerate: false` → `codeSamples: false`. `x-codeSamples` in the spec needs nothing: Blume renders them ahead of the generated samples.\n- **Keep the \"Introduction\" page.** Mintlify commonly has a written intro/auth page in an \"Introduction\" group beside the \"Endpoints\" (openapi) group in the same tab. Keep it: a normal content page placed under the `openapi()` adapter's `route` (e.g. `<root>/api-reference/introduction.mdx`) merges into the reference tab's sidebar alongside the generated operations. Delete only the per-endpoint stubs, not the conceptual pages.\n\n## Assets\n\nMintlify serves every top-level dir (e.g. `/images`) at the site root. Blume serves `public/` at the site root. **Move root asset dirs into `public/`** (`mv images public/images`) — every `/images/...` reference still resolves, unchanged. Move loose root files (`logo.png`, `favicon.png`) under `public/` too.\n\n## i18n\n\n`navigation.languages` (≥2) → `i18n: { defaultLocale, locales: [{ code, label }] }`. The `default: true` language → `defaultLocale`. Translated content already lives in ISO-code directories, which match Blume's `dir` parser — no file moves. Remove any language selector; language switching is handled by i18n.\n\nPer-language `banner`, `navbar`, and `footer` (on `navigation.languages[]`) merge into the one site-wide config, with each label as a map of language code to that language's text: `banner: { content: { en: \"…\", fr: \"…\" } }` (and `link.text`), `navigation.actions`/`cta` labels, and `footer` link labels. Hrefs stay single — Blume moves an internal link into the reader's language itself. A language without an entry shows the default language's text. Report what doesn't merge: a banner or link set for only some languages, or one whose href differs per language.\n\n## Dropped — report these\n\n- **A group's `boost`** (on a `navigation` group, inherited by its pages) → no inherited boost; set `search.boost` on the pages that need it.\n- **Authentication** (password or SSO, set in the Mintlify dashboard) → Blume has none of its own; point the user at host-level protection (Vercel Deployment Protection, Netlify password protection, Cloudflare Access), per the deployment docs' **Private docs** section. It covers the whole site, so a mix of public and private pages needs two sites.\n- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`.\n\n<!-- File: .agents/skills/blume-migrate/references/mkdocs.md (Tokens: ~9260 | Category: anthropic-skill) -->\n# MkDocs (Material for MkDocs, Zensical) → Blume\n\nMkDocs is a Python generator: one YAML config (`mkdocs.yml`), Markdown in `docs/`, and Python-Markdown extensions for everything beyond CommonMark. Nearly every MkDocs site uses the **Material for MkDocs** theme, so pages are full of extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets, `{ #id }` attribute lists), and navigation is an explicit `nav` list in config (or alphabetical). The work is translating `mkdocs.yml`, rebuilding `nav` as folders and `meta.ts`, and converting the syntax — most of which becomes `.mdx`-only Blume features, and **MDX rejects several things Python-Markdown accepts**.\n\nScope: checked against MkDocs 1.6.1, Material for MkDocs 9.7.6–9.7.7, pymdown-extensions 11.0.1–12.1, mkdocs-redirects 1.2.3, awesome-pages 2.10.1, awesome-nav 3.3.0, literate-nav 0.6.3, mkdocs-static-i18n 1.3.1, mkdocs-macros 1.5.0, mike 2.2.0, an end-to-end migration of astral-sh/uv (84 pages), and the codemod on pdm-project/pdm (23 pages). **Zensical** and **ProperDocs** projects are the same migration. MkDocs 2.0 (a pre-release with a TOML config and no plugins) was not checked.\n\n## Run the codemod first\n\nFrom the folder that holds `mkdocs.yml` (keep the old Python setup until the end, so `mkdocs build` can still produce the old site for comparison):\n\n```bash\nnode <skill>/scripts/mkdocs-codemod.mjs docs           # dry run: report only\nnode <skill>/scripts/mkdocs-codemod.mjs --write docs   # apply\n```\n\nIt reads `mkdocs.yml` (or `zensical.toml`/`properdocs.yml`, `INHERIT` included) for the enabled extensions, the snippets `base_path`, `extend_pygments_lang`, and `nav` titles, then per page: converts admonitions, details, and tabs (indentation-aware, nested); turns snippets into `<include>` (copying ones from outside the content root into `_snippets/`); rewrites fences, `#!lang` inline code, heading ids, attribute lists, keys, `==mark==`/`^^ins^^`, icon shortcodes, `\\(x\\)` math, and abbreviation lines; moves the body H1 (ATX or setext) into `title` (inline Markdown stripped — Blume renders backticks in `title` literally), a differing `nav` title into `sidebar.label`, and a differing frontmatter `title` into `seo.title`; maps `hide`/`status`/`tags`/`icon`; renames to `.mdx` **only** the pages that now need MDX and fixes their comments, autolinks, and void tags; renames `README.md` to `index.md`; and rewrites links and `<include>`s to every renamed file. It is idempotent.\n\nIt **reports**, without changing, what needs judgment: mkdocstrings blocks, macros/Jinja, template overrides, hooks, every plugin (with a hint), mike and `extra.alternate`, snippet line ranges, `///` blocks, grid cards, code annotations, unknown icons, raw `<img>`s with a relative `src`, pages left with no title, snippet files that publish as pages too, `nav` entries with no file, and every MDX hazard left in a renamed page. Work through that report with the sections below. Nav, config, redirects, and repo integration stay yours.\n\nAfter the first `blume build`, pin the old heading ids (MkDocs and Blume slug headings differ — see URLs). Point `--old` at the old site's built HTML (`mkdocs build -d <dir>` before you delete `mkdocs.yml`) or its live URL, and run it from the Blume project:\n\n```bash\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url>           # dry run\nnode <skill>/scripts/pin-heading-ids.mjs --old <old-build-dir-or-url> --write\n```\n\nRebuild and rerun until it reports 0 (add `--map dist/blume-redirects.json` when pages moved).\n\n## Detect\n\n- **`mkdocs.yml` / `mkdocs.yaml`** — Material when `theme.name: material`; the plain `mkdocs`/`readthedocs` themes migrate the same way with fewer extensions. Sometimes in a subfolder (`docs/mkdocs.yml`), which `blume migrate` doesn't detect from the repo root: name the source, and run the codemod from that folder.\n- **`zensical.toml`** — Zensical's format: MkDocs' keys in TOML, usually under `[project]` (`[project.theme]`, `nav = [ { \"Title\" = \"page.md\" } ]`); extensions as `[project.markdown_extensions]` keys (`admonition = {}`, `pymdownx.tabbed.alternate_style = true`) or `[project.markdown_extensions.<name>]` tables. With **no extensions configured**, Zensical enables a default set (admonition, details, tabbed, attr_list, md_in_html, keys, mark, caret, arithmatex, emoji, and more, but not snippets); the codemod applies it to a `zensical.toml`. Zensical also builds a plain `mkdocs.yml` and applies the same defaults when it sets no `markdown_extensions`: the codemod can't tell, so list the extensions the pages use in a copy of the config and pass it with `--config`.\n- **`properdocs.yml` / `.yaml`** — ProperDocs, a continuation of MkDocs 1.x.\n- Python deps, not `package.json`: `mkdocs`, `mkdocs-material`, `zensical`, `properdocs` in `requirements*.txt` or a `pyproject.toml` docs group; `mkdocs build`/`gh-deploy` in CI; `.readthedocs.yaml` with an `mkdocs:` key.\n\nResolve the config first: `INHERIT: base.yml` deep-merges this file over another (a repo may have several configs on one base — migrate the public one, report the rest). `!ENV [VAR, default]` reads an environment variable: use the default and report it. `!relative` and `!!python/name:` tags break JS YAML parsers — read the file as text.\n\n**Where the Blume project goes.** `blume build` empties `<project>/dist`, and a Python or Rust repo root often uses `dist/` for wheels or builds. When `mkdocs.yml` sits at the root of a code repo, put `blume.config.ts` and `package.json` **in the docs folder** with `content.root: \".\"` and `content.include` scoped to the page folders (`[\"index.mdx\", \"{guides,concepts,reference}/**/*.{md,mdx}\"]`), and say so. That also keeps a `package.json` out of a non-JS repo root.\n\n**Generated pages.** A `nav` entry or link whose file isn't in the repo is generated at build time (`gen-files`, a hook, a CI step, a `.gitignore`d output — the codemod lists them). Convert today's output once so the site builds, then report that the **generator must change**: until it writes Blume syntax, CI regenerates the old one and Blume renders it as text with a green build. If conversion renames the output (`settings.md` → `settings.mdx`), update `.gitignore`, `.prettierignore`, and the generator's own check mode too.\n\n## Config: `mkdocs.yml` → `blume.config.ts`\n\nMap only what's set:\n\n| MkDocs | Blume |\n| --- | --- |\n| `site_name` / `site_description` | `title` / `description` |\n| `site_url` | Its origin → `deployment.site` (unless the target is Vercel or Netlify — see SKILL.md); **a path in it** (`https://docs.acme.com/uv/`) → `deployment.base: \"/uv\"`, on every host. mike adds its version segment after `site_url`, so it isn't part of the base (Versioning) |\n| `docs_dir` | `content.root` (`docs` is the default) |\n| `use_directory_urls` | no setting — see URLs |\n| `repo_url` on GitHub | `github: { owner, repo }` |\n| `edit_uri` / `edit_uri_template` | **`github.branch`** from the segment after `edit/` or `blob/`. Unset with a GitHub `repo_url` means `edit/master/docs/` → `branch: \"master\"` (Blume defaults to `main`). The path after the branch is `docs_dir`, which Blume adds — **not** `github.dir` (that's only for a project in a repo subfolder). `edit_uri: \"\"`, or Material without the `content.action.edit` feature, shows no edit button; Blume has no separate switch, so accept edit links or leave `github` unset and set `navigation.repo: \"<repo_url>\"` (a footer mark) — say which. Leave `github` unset if edit links would point at gitignored generated files |\n| `repo_url` elsewhere (GitLab…) | no edit links; a header link → `navigation.actions` (report) |\n| `repo_name`, `site_author`, `site_dir`, `dev_addr`, `remote_*`, `watch`, `strict`, `validation` | drop |\n| `copyright` | `footer.copyright`, plain text: strip any HTML tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`) |\n| `exclude_docs` | `content.exclude` — the list adds to the default `\"**/_*\"` and `\"**/.*\"`; `\"!**/_*\"` publishes `_` files |\n| `draft_docs` / `not_in_nav` | `draft: true` / `sidebar.hidden: true` on those pages |\n| `extra_css` | a project-root **`theme.css`**: Material's `--md-*` variables and `.md-*` classes don't exist — port only rules that still mean something, and read colors out of it (Theme) |\n| `extra_javascript` | delete MathJax/KaTeX/Mermaid loaders. Anything else → move into `public/` and `script({ src })` in `analytics` (from `blume/analytics`); a root-relative `src` (`\"/js/x.js\"`) loads under `deployment.base`. Code written against Material's `document$` or `.md-*` DOM won't run — report it |\n| `extra.social` | `footer.socials` (bluesky, discord, facebook, github, hacker-news, instagram, linkedin, medium, podcast, reddit, slack, telegram, threads, website, x, youtube; `twitter`/`x-twitter` → `x`); the site's own repo is covered by `github`; other platforms drop (report) |\n| `extra.analytics` (`provider: google`) | `googleAnalytics({ id: property })`. A `custom_dir` partial `partials/integrations/analytics/<name>.html` is a custom provider: use its adapter (`fathom({ site })` from `data-site`, …) or `script()`. `feedback` → Blume's page feedback is on by default |\n| `extra.consent` | `consent: native()` (from `blume/consent`) |\n| `extra.version` (`provider: mike`) | Versioning |\n| other `extra` keys used as `{{ }}` | `variables` (Manual decisions: macros) |\n| `markdown_extensions` | nothing to configure. `toc.toc_depth: N` → `toc: { maxHeadingLevel: N }` (a `\"2-5\"` range: its upper bound) |\n| `plugins`, `hooks`, `theme.custom_dir` | Plugins; Manual decisions |\n\n### Theme\n\n| Material | Blume |\n| --- | --- |\n| `theme.palette` | one palette with `scheme: slate` → `theme.mode: \"dark\"`; with `default` → `\"light\"`; a list with toggles → system (the default) |\n| `palette.primary` (link color) | `theme.accent`. Presets `red`, `pink`, `purple`, `blue`, `green`, `orange`, `teal` map by name. Other names aren't usable CSS (`deep purple` warns `BLUME_THEME_COLOR_INVALID`, CSS `indigo` isn't Material's): use Material 9.7's link color (`--md-typeset-a-color`) as `{ light, dark }` — indigo `#4051b5`/`#5488e8`, deep purple `#7e56c2`/`#a47bea`, light blue `#02a6f2`, cyan `#00bdd6`, deep orange `#ff6e42`/`#ff764d`, brown `#795649`/`#c1775c`, amber `#d19d00`/`#ffc105`, lime `#8b990a`/`#cbdc38`, light green `#72ad2e`/`#8bc34b`, yellow `#b8a500`/`#ffec3d`; grey, blue grey, black, and white all link in `#4051b5`/`#5e8bde`. `custom` or a custom `scheme:` is defined in `extra_css`: take `--md-typeset-a-color` per scheme (and `--md-default-bg-color` → `theme.background` when it isn't plain white/black) |\n| `palette.accent` | drop |\n| `theme.font.text` / `.code` | `theme.fonts.body` (and `display`) / `mono` as `{ name: \"…\" }`; `font: false` → drop |\n| `theme.logo` (a file in `docs_dir`) | move into `public/`, `logo: \"/logo.svg\"` (SKILL.md on `currentColor` and wordmarks). `theme.icon.logo` → drop and report |\n| `theme.favicon` | move to `public/favicon.<ext>` — no config field |\n| `theme.icon.*`, `theme.language` (English) | drop. A non-English single-language site: the schema accepts a one-locale `i18n` block — check the UI language in `blume dev` |\n\n`theme.features`:\n\n| Feature | Blume |\n| --- | --- |\n| `navigation.tabs` | **`navigation.tabs`**, one per top-level `nav` section, each in one folder (SKILL.md \"Tabs\") |\n| `navigation.sections` | top-level sections stay Blume's default `flat` headers; set **`display: \"group\"` in the `meta.ts` of every second-level section**: Material renders those as collapsible, and a `flat` group lists its pages above its subgroups, reordering a section that interleaves them |\n| neither `sections` nor `tabs` | `navigation.sidebar.display: \"group\"` (Material's collapsible tree) |\n| `navigation.expand` | `collapsed: false` in those folders' `meta.ts` |\n| `navigation.indexes`, plugin `section-index` | a folder's `index` page links its group header. Blume also lists it as the group's first row, so the label shows twice. To show it once, as Material does, set `sidebar.hidden: true` on the index: that drops only the duplicate row, and the page stays in `sitemap.xml`, search, and `llms.txt`. A hidden index whose `title` differs from its folder's `meta.ts` `title` warns `BLUME_NAV_INDEX_TITLE_MISMATCH`, which fails `validate --strict`: match the two titles, set the index's `sidebar.label` to the folder title to keep its heading, or leave that index's row showing |\n| `content.tabs.link` | same-titled tabs sync within a page, not across pages |\n| `content.action.edit` / `.view` | `github` |\n| `announce.dismiss` | `banner.dismissible`; the text is `main.html`'s `{% block announce %}` → `banner.content` |\n| `content.code.annotate` | no equivalent (the codemod reports annotation markers) |\n| `content.code.copy`/`.select`, `navigation.footer`/`.path`/`.top`/`.instant*`/`.tracking`/`.prune`, `toc.*`, `search.*`, `header.autohide`, `content.tooltips` | drop — built in or no equivalent. Blume's copy button copies only the commands of a `console` or `shellsession` block, without prompts or output, so a custom script that stripped them (`extra_javascript`) can go once those blocks use one of the two |\n\n## Navigation\n\nBlume builds the sidebar from folders (SKILL.md \"Navigation is the file tree\"). Translate `nav` into folders and `meta.ts` (the codemod already set each page's `title`/`sidebar.label`):\n\n- **`- Section: [ … ]`** → a folder; a label that differs from the humanized folder name (\"Getting started\" vs \"Getting Started\") → `meta.ts` `title`. List order → `meta.ts` `pages`, every child. Root order → the content root's `meta.ts`.\n- **A section holding an `index.md`** (with `navigation.indexes`) has that page as its own → nothing to do (see `navigation.indexes` above). Without the feature, Material lists the index as an ordinary row, which is what Blume does anyway.\n- **Nesting that doesn't match folders** is SKILL.md's \"Config-declared nesting\". Decide per section and report:\n  - **One flat folder split into subsections** (`Advanced:` listing `guides/webhooks.md`): a **`(group)/` folder** first, as SKILL.md says — `guides/(advanced)/webhooks.md` keeps `/guides/webhooks` and adds the sidebar group, with no redirects.\n  - **A folder shown under another section** (uv's `pip/` under \"Concepts\"), where a group folder would change the URL segment: **move the files and pin `slug: <old route>` on each moved page**. The sidebar follows the file and the URL follows the slug, with no redirects and no explicit sidebar. Use it when the source kept URLs on purpose.\n  - Otherwise, move the files and add `redirects` (the URLs change), or declare the shape in an explicit `navigation.sidebar` (it replaces the whole generated tree).\n  - Either move puts a page a folder deeper: re-base its relative links and assets, and rewrite every link to it to name the file as it now is (a link to `page.md` resolves to a slugged page only when it names that exact file).\n- **`- Title: https://…`** → `navigation.featured` (top level) or `navigation.actions`; a `nav` entry to a non-page file (`llms.txt`, a PDF) → a featured link.\n- **Pages left out of `nav`** → `sidebar.hidden: true` (note: this also drops them from `sitemap.xml`).\n- **No `nav`**: MkDocs lists each folder's pages (`index` first, then by filename) before its subfolders, labeled \"Getting started\"-style; Blume sorts by title. Add `meta.ts` `pages`/`title` where that differs.\n- **`awesome-pages` `.pages` / `awesome-nav` `.nav.yml`** → a `meta.ts` per folder: `title` → `title`; `nav` → `pages` (drop awesome-pages' `...` and awesome-nav's glob entries like `\"*\"`; an awesome-pages `nav` without `...` hides everything unlisted → `sidebar.hidden: true`; `- Title: page.md` → `sidebar.label`); `hide: true` → hide its pages; sorting, `append_unmatched`, and `collapse*` options drop (report). Delete the files.\n- **`literate-nav`** (`SUMMARY.md`): its nested link list is the nav — convert it like `nav` and delete it (it would publish as a page).\n\n## URLs and redirects\n\nSave the old URL list first — every `<loc>` in the old `site/sitemap.xml` (or the live one) plus every `redirect_maps` key — and check it at the end.\n\n| Source file | MkDocs URL (`use_directory_urls: true`) | (`false`) | Blume route |\n| --- | --- | --- | --- |\n| `docs/a/b.md` | `/a/b/` | `/a/b.html` | `/a/b` |\n| `docs/a/index.md` | `/a/` | `/a/index.html` | `/a` |\n| `docs/a/README.md` | `/a/` | `/a/index.html` | `/a/README` until renamed `index.md` (the codemod does) |\n| `docs/01-setup.md` | `/01-setup/` | `/01-setup.html` | **`/setup`** — Blume strips numeric prefixes; redirect, or pin `slug: 01-setup` |\n\n- **Trailing slashes**: a static build writes `<route>/index.html`, so slashed URLs keep working on static hosts; `vercel()` 308s them; Cloudflare Workers needs `html_handling: \"drop-trailing-slash\"`. `blume dev` and `blume preview` redirect a slashed URL to the slashless page, so old URLs can be checked with or without the slash. **Write targets in any client-side script** (anchor-redirect maps) slashless: Blume's URLs have none.\n- **`use_directory_urls: false`**: one exact redirect per non-index page (`{ from: \"/a/b.html\", to: \"/a/b\" }`; a pattern can't strip a suffix). Hosts that read `_redirects`/`vercel.json` answer them; GitHub Pages, which reads neither, is unverified.\n- **`redirects` plugin**: each `redirect_maps` key and value is a Markdown path relative to `docs_dir` → `{ from: <route of key>, to: <route of value> }`, `#anchor` kept; an `http(s)` value stays a URL.\n- **Anchor-level redirects** (a script mapping `/old/#section`): servers never see fragments. Keep the script (`script({ src })`, targets slashless) or drop it; report either way.\n- **Heading ids**: MkDocs' default slugify strips accents, collapses punctuation runs (`A & B` → `a-b`), and numbers duplicates `_1`; Blume keeps accents, writes `a--b`, and numbers `-1`. Run `pin-heading-ids.mjs` (above) — don't compare by hand.\n- **Links**: the codemod rewrites links to renamed files. After you move or rename anything yourself, point links at the real filenames again — a `page.md` link to a file now named `page.mdx` still lands on its page (Blume tries the other extension, `slug` included), but breaks wherever the Markdown is read as files, such as on GitHub. Links inside an included partial resolve from the **including** page, as with MkDocs snippets. A link to a file moved into `public/` becomes root-relative. A link MkDocs couldn't map to a file (`../install/`, written against MkDocs' slashed URLs) lands one level higher in Blume on any page that isn't an `index`, because Blume reads relative links from the page's folder: point it at the `.md` file. `blume validate --strict` catches the rest. The codemod percent-encodes angle-bracket destinations (`[x](<https://…(…)>)` → `…%28…%29`), which only tidies: Blume reads both forms, and rebases a partial's `<…>` image path onto the including page.\n- **llms.txt plugins**: Blume writes `/llms.txt` and `/llms-full.txt`. The plugin's `/<route>/index.md` copies become `/<route>.md`: add one exact redirect each (a pattern can't map them; `blume audit --only redirects` accepts `.md` targets), and reword any URL advice carried into `agents.llmsTxt.details`.\n\n## Which pages become `.mdx`\n\nDirectives, components, math, and Mermaid work only in `.mdx`, and so does an `<include>` indented inside a list item (a `.md` page reads it as indented code and ships the bare tag); the codemod renames exactly the pages that use them (or include a partial that does) and keeps the rest `.md`. MDX **fails the build** on Python-Markdown-valid source, and the codemod reports what's left in renamed pages: `<!-- -->` comments (→ `{/* … */}` on **one line**), `<https://…>` autolinks, unclosed `<br>`/`<img>`, `<placeholder>` text in prose (`uv run <command>` → backticks), and any `{…}` in prose — attribute lists, critic markup, Jinja, `{name}`. Most fail to compile, and `blume check` (and `validate`, `dev`, `build`) names each at its line as `BLUME_MDX_SYNTAX`; it also names an attribute list (`BLUME_MDX_ATTRIBUTE_LIST`: move the attributes onto a JSX element, or drop them) and an unclosed void tag (`BLUME_MDX_UNCLOSED_ELEMENT`: close it, `<br />`). `{ width=\"300\" }`, `{target=\"_blank\"}`, and `{name}` compile and then fail the render, which `blume build` reports as `BLUME_MDX_UNDEFINED_NAME` at the expression's line. In a `.md` page none of these fail and no extension syntax renders: it ships as text (`--8<--` becomes \"–8<–\", `*[HTML]: …` shows verbatim), and an attribute list warns `BLUME_MD_ATTRIBUTE_LIST`. Smart punctuation turns prose `--flag` outside backticks into an en dash.\n\n## Content\n\nWhat the codemod converts, for checking its output, and what it leaves for you:\n\n| MkDocs | Blume |\n| --- | --- |\n| `!!! type \"Title\"` (body indented 4) | `:::type[Title]` … `:::` (longer `::::` outside nested ones). Material styles 12 types; others render like `note`. Mapped: `abstract`/`question` → `info`, `failure`/`bug` → `danger`, `example`/`important`/`todo` → `note`, `caution`/`attention` → `warning`, `hint` → `tip`, `quote` → a blockquote. Untitled callouts lose Material's \"Note\" title bar; a bracket title keeps its inline Markdown (an unbalanced `]` flattens it) |\n| `??? type \"Title\"` / `???+` | `<Expandable title=\"Title\">` / `defaultOpen`; the type's color drops, and the title keeps its inline Markdown (the codemod strips raw HTML, which would show as text) |\n| `=== \"Tab\"` | every tab one code block → `<CodeGroup>` with the label as `title=\"…\"`; otherwise `<Tabs>`/`<Tab title>` (`===+` → `defaultTabIndex`, `===!` → a new group) |\n| `hl_lines=\"2 4-5\"`, `linenums=\"1\"`, `{ .py title=\"x\" }`, options without a language | `{2,4-5}`, `lineNumbers` (a start other than 1 drops), `py title=\"x\"`, `text {…}`. One left over warns `BLUME_CODE_FENCE_OPTION` (with the Blume spelling) or `BLUME_UNKNOWN_CODE_LANGUAGE` |\n| `pycon`, `pwsh-session`, `extend_pygments_lang` names | `python`, `powershell`, the mapped lexer (any other name Shiki lacks warns `BLUME_UNKNOWN_CODE_LANGUAGE`: map it or write `text`) |\n| `` `#!python x()` `` | `` `x(){:python}` `` |\n| `{ #id }`, `{: #id }`, `{#id}` on headings, also beside classes (`{ #id .wide }`) | `[#id]`, classes dropped (a self-link like `### [x](#x) {: #x }` drops). One left over warns `BLUME_MD_CURLY_ANCHOR` in `.md` (only `{#id}` pins there), and `BLUME_MDX_CURLY_ANCHOR` in `.mdx` (`BLUME_MDX_ATTRIBUTE_LIST` beside classes): write `[#id]` |\n| other attribute lists | dropped and reported (one left over warns `BLUME_MD_ATTRIBUTE_LIST` in `.md`): buttons → links or `<Card>`s (a homepage CTA → `navigation.cta`); image sizing → `<img … />` or `<Frame>`; `{target=\"_blank\"}` → `markdown.externalLinks: true` |\n| `++ctrl+c++`, `==x==`, `^^x^^` | `<kbd>Ctrl</kbd>+<kbd>C</kbd>`, `<mark>`, `<ins>` |\n| `:material-check:` (mapped icons) | `<Icon icon=\"check\" />`; `:octicons-link-external-16:` drops (see `externalLinks`) |\n| `\\(x\\)`, `\\[`…`\\]` | `$$x$$`, `$$` blocks |\n| `*[HTML]: …`, `[TOC]` | removed |\n| `<div markdown>` | attribute dropped (Markdown inside renders in `.mdx`) |\n| Body `# H1` | frontmatter `title` |\n| **By hand:** single-`$` math | `$$…$$` inside the sentence, where it is math |\n| definition lists | `<dl>` or a list |\n| `///` blocks (pymdownx.blocks) | the same targets as `!!!`/`???`/`===`; `/// caption` → `<Frame caption>` |\n| grid cards (`<div class=\"grid cards\" markdown>`) | `<CardGroup>`/`<Card>` |\n| code annotations (`# (1)!` + a list) | prose after the block |\n| `![](x#only-light)` / `#only-dark` | a `theme.css` rule: `:root:not([data-theme=\"dark\"]) img[src$=\"#only-dark\"], :root[data-theme=\"dark\"] img[src$=\"#only-light\"] { display: none; }` (checked in a browser) |\n| emoji `:smile:`, magiclink `#123`/`@user` | paste the emoji; write the links |\n| unmapped icons | a Lucide name in `<Icon>` (verify at lucide.dev), or drop; brand icons (Python, Linux, Docker) have none |\n\n### Snippets and includes\n\n- Snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), never the page; the codemod rewrites them to `/`-rooted `<include>`s.\n- Includes must live in the content root. The codemod copies outside files (`CONTRIBUTING.md`, `examples/`) into `_snippets/`; if the source keeps changing, ship a small `predev`/`prebuild` script that re-copies it, and gitignore the copy. Drop a copied file's H1 (the codemod reports it) — the page's title already renders one.\n- Line ranges (`file.py:3:10`) have no `<include>` equivalent: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (`lines`, run before every build so it keeps tracking the source). Sections (`file.md:name`, marked `--8<-- [start:name]`) aren't read by that script: split the section into its own file and report it.\n- `auto_append` link-definition files: `<include>` the file at the end of each page that uses them. An included file's definitions resolve references in the including page, as if written inline (and the page's resolve in the partial).\n- A partial is parsed with the page that includes it: its directives render only in an `.mdx` page. An HTML comment in a `.md` partial stays hidden in an `.mdx` page too.\n- A snippet file that isn't under a `_` or `.` folder or name is a page too, and publishes on its own as it did in MkDocs; the codemod reports each one. Move it under `_snippets/` unless it should stay a page.\n- `include-markdown` (`{% include \"./x.md\" %}`): `./` paths are page-relative like Blume's, bare ones `docs_dir`-relative (→ `/x.md`); `start`/`end` excerpts → their own file.\n\n## Frontmatter\n\nThe codemod maps `hide: [toc]` → `mode: wide`, `hide: [navigation]` → `mode: center`, `hide: [footer]` → `pagination: false`, `status: new` → `sidebar.badge: New` (`deprecated` → `deprecated: true`), `tags` → `search.tags` (a hosted-search facet; tag pages drop), `icon: material/…` → a Lucide name, and drops `subtitle`, `template`, `social`, `comments`, `render_macros`. `search.exclude`/`boost` keep their meaning. It reports every other key Blume's strict schema rejects (`BLUME_FRONTMATTER_INVALID`) — map or remove them; `.meta.yml` defaults (the `meta` plugin) apply to every page in their folder.\n\n## Plugins — map, don't drop\n\n| Plugin | Blume |\n| --- | --- |\n| `search` | built in — delete |\n| `redirects` | `redirects` (URLs) |\n| `git-revision-date(-localized)` | `lastModified: \"git\"` (it shows a visible \"Last updated\" line) |\n| `git-committers`, `git-authors`, `minify`, `privacy`, `offline`, `typeset`, `info` | drop |\n| `social`, `glightbox`, `optimize` | built in: Open Graph images, image zoom, image optimization |\n| `group` | unwrap and map its plugins |\n| `exclude` | `content.exclude` |\n| `meta`, `tags` | Frontmatter |\n| `blog`, `rss` | Blog |\n| `awesome-pages`, `awesome-nav`, `literate-nav`, `section-index` | Navigation |\n| `include-markdown` | Snippets |\n| `llmstxt` | built in: `markdown_description` → `agents.llmsTxt.details` (reword URL advice); `sections` drop |\n| `macros`, `mkdocstrings`, `mkdocs-click`, `gen-files`, `mkdocs-jupyter` | Manual decisions |\n| `i18n`/`static-i18n`, `mike` | i18n; Versioning |\n| anything else | read what it does; report it |\n\n## Manual decisions — stop and report\n\n- **mkdocstrings** (`::: package.module`): no Python API generator in Blume. Keep that reference published elsewhere and link to it, use `openapi()` for an HTTP API, or hand-write the key pages; redirect the URLs you drop. The same for `mkdocs-click` CLI references and `gen-files` output.\n- **macros**: plain values (`extra`, `include_yaml`, `{% set %}`) → `variables`, same `{{ name }}` syntax — names take letters, digits, `_`, `-` only, and `{{ config.x }}`/`{{ page.x }}` must be inlined. `{% include 'x.md' %}` (from `include_dir`, default `docs_dir`) → `<include>/x.md</include>`. Macro calls and control flow → expand into plain content (a one-off render script is fine); strip `{% raw %}` wrappers.\n- **Template overrides** (`custom_dir`) and **hooks**: read each. Announcement → `banner`; `<head>` meta tags → `seo.metatags` (minus the tags Blume owns); analytics partials → adapters; `<title>`/JSON-LD tweaks → Blume's own; the rest → layout slots (`defineComponents`) or `blume eject`.\n- **Notebooks** (`mkdocs-jupyter`): export to Markdown or link out.\n\n## i18n\n\n`static-i18n` `languages` → `i18n: { defaultLocale, locales: [{ code, label }] }` (both serve the default at the root, others under `/<locale>/`). `docs_structure: suffix` (`page.fr.md`, the default) → `parser: \"dot\"`; `folder` → the default `dir` parser with the **default locale's folder moved up to the content root**. `nav_translations` → per-locale `meta.ts` titles; `fallback_to_default` matches `fallbackLocale`. Pin translated headings with `[#id]`.\n\n## Versioning (`mike`)\n\nmike publishes built HTML per version (`/<version>/…`, aliases like `/latest/`); old versions' sources live only in git tags. Migrate the current version unless asked: serve it at the root and redirect `/<alias>/:path*` and `/<version>/:path*` to `/:path*`. Older versions stay on the old host (a `navigation.featured` link) or are converted from their tags into Blume's `versions.archived` (ids start with a letter). Report which.\n\n## Blog (Material `blog` plugin)\n\nPosts in `blog/posts/` → `type: blog` pages. `date: { created, updated }` → `date` + `lastModified`; `authors: [id]` → inline objects from `.authors.yml`; `categories` → `search.tags` or drop. Keep the old dated URL with a full-route `slug: blog/2024/01/31/my-post`, or redirect. Delete `<!-- more -->` in `.mdx`. Blume has no blog index, archive, category, or pagination pages: write `blog/index.mdx` (`docs/advanced/blog.mdx`), report the rest. RSS moves to `/blog/rss.xml`; with the `rss` plugin (mkdocs-rss-plugin), redirect its `/feed_rss_created.xml` and `/feed_rss_updated.xml` (and `/feed_json_*.json`) there.\n\n## Repo integration and teardown\n\n- **Assets**: everything non-Markdown in `docs_dir` that's served by URL — logo, favicon, downloads, `CNAME`, verification files — moves to `public/`. Images and other files that pages link relatively can stay: Blume publishes each with the page, though at a new URL, so a download whose old URL is linked from elsewhere moves to `public/` too.\n- **Renamed files**: after the codemod, **grep the whole repo for the old page paths** — release tooling that bumps versions inside pages (`[tool.rooster] version_files`, `bumpversion`), Renovate/Dependabot file globs, CODEOWNERS, `.prettierignore`, CI path filters, and scripts.\n- **Prettier**: `.prettierrc` and `.editorconfig` overrides keyed on `*.md` stop applying to the new `.mdx` files — extend them to `*.mdx` before reformatting. Prettier turns a multi-line `{/* … */}` into `{/_ … _/}` (keep MDX comments on one line) and rewrites a URL containing parentheses as a `<…>` destination (fine in a page; percent-encode it in a partial, see Links). If the repo uses Ultracite/oxfmt, see `references/monorepo.md` §6.\n- **CI**: replace `mkdocs build`/`gh-deploy` with Node 22, an install, `blume build`, and `blume validate --strict` (GitHub Pages: the \"Deploy Markdown docs to GitHub Pages\" guide); a `.readthedocs.yaml` build is a hosting change to report. Add `.gitignore` entries for `node_modules/`, `.blume/`, `dist/`, and `.env.local`.\n- **Teardown**: delete `mkdocs.yml` (and `INHERIT`ed bases), `overrides/`, `.pages`/`.nav.yml`/`SUMMARY.md`/`.meta.yml`, hooks, and `extra_css`/`extra_javascript` files once ported. Leave the Python docs dependencies for a follow-up: removing a `pyproject.toml` docs group needs a lock regeneration (`uv lock`), and CI running with `--locked`/`UV_LOCKED=1` fails until it's done — report it.\n\n## Leftover check\n\nRerun until it prints only what you mean to keep (code samples, and `{{name}}` variables you defined):\n\n```bash\ngrep -rnE '^\\s*(!!!|\\?\\?\\?\\+?|===[+!]*) |^\\s*/{3,} ?[a-z]|--8<--|\\{ *[.#:][^}]*\\}|\\{\\{|\\{%|<!--|:(material|octicons|fontawesome|simple)-[a-z0-9-]+:|\\+\\+[a-z]|==[^= ][^=]*==|\\{\\+\\+|\\{--|^\\*\\[|`#![a-z]|hl_lines=|linenums=' docs\n```\n\n## Dropped — report these\n\nTemplate overrides and hooks with no slot equivalent, links inside `copyright`, code annotations, admonition title bars and collapsible callout colors, `inline` admonition placement, `palette.accent`, unmapped and brand icons, abbreviation tooltips, definition-list styling, snippet line ranges and sections (split into files), tag index pages, blog archive/category/pagination pages, anchor-level redirects without the script, cross-page tab memory, git committers, mkdocstrings/click/gen-files/notebook output not carried over, macro functions and Jinja logic, social platforms `footer.socials` lacks, a `linenums` start offset, image sizing from attribute lists, and every plugin you removed without an equivalent.\n\n<!-- File: .agents/skills/blume-migrate/references/monorepo.md (Tokens: ~4589 | Category: anthropic-skill) -->\n# Migrating into a real repo (monorepos, pnpm, Vercel)\n\nThe framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.\n\n**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.\n\n---\n\n## 1. Non-standard content layout → scope `content.root` + `include`\n\n`content.root` defaults to `\"docs\"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.\n\n**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:\n\n```bash\n# top two path segments of every content file, deduped — your content folders\nfind . -path ./node_modules -prune -o \\( -name '*.md' -o -name '*.mdx' \\) -print \\\n  | sed 's|^\\./||' | cut -d/ -f1 | sort -u\n```\n\nIf that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.\n\n**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: \".\"` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:\n\n```ts blume.config.ts\nimport { defineConfig } from \"blume\";\n\nexport default defineConfig({\n  content: {\n    root: \".\", // content sits directly under this package (apps/docs/), no docs/ subfolder\n    // include/exclude globs are relative to `content.root`. List the actual\n    // content folders so the scan never wanders into src/, public/, etc.\n    include: [\n      \"api/**/*.{md,mdx}\",\n      \"getting-started/**/*.{md,mdx}\",\n      \"guides/**/*.{md,mdx}\",\n    ],\n    exclude: [\"**/node_modules/**\"], // added to the default \"**/_*\" and \"**/.*\"\n  },\n});\n```\n\nSchema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `[\"**/*.{md,mdx}\"]`), **`content.exclude`** (array of globs, relative to `content.root`, added to the default `[\"**/_*\", \"**/.*\"]`; a `!`-prefixed default, `\"!**/_*\"`, takes it back out). Singular `include`/`exclude`, both arrays. A folder `meta.ts` outside every `include` glob is never read (`BLUME_META_OUTSIDE_INCLUDE` names it), so a folder that holds only a `meta.ts`, like an API reference's tag folder, needs a glob of its own (`\"api-reference/**/*.{md,mdx}\"`).\n\nReport to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.\n\n---\n\n## 2. Release-age guards (pnpm `minimumReleaseAge`, Yarn `npmMinimalAgeGate`) block installing fresh Blume\n\npnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.\n\n**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.\n\n**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:\n\n```bash\ngrep -rn \"minimumReleaseAge\\|minimum-release-age\" pnpm-workspace.yaml .npmrc 2>/dev/null\n```\n\n**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):\n\n```yaml\nminimumReleaseAge: 1440\nminimumReleaseAgeExclude:\n  - blume\n```\n\n**Fix — `.npmrc`** form (if the policy lives there instead):\n\n```ini\nminimum-release-age=1440\nminimum-release-age-exclude[]=blume\n```\n\nReport: \"Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else.\"\n\n**Yarn ≥4.10** has the same guard: `npmMinimalAgeGate` in `.yarnrc.yml` (e.g. `2w`). Its per-package exemption is `npmPreapprovedPackages`. Yarn reads rc files from parent directories, so a nested `docs/` package inherits the root's gate. Detect with `grep -rn \"npmMinimalAgeGate\" .yarnrc.yml docs/.yarnrc.yml 2>/dev/null`, then add `npmPreapprovedPackages: [\"blume\"]` (or append `blume` to the existing list) in the file that sets the gate. Don't remove the gate.\n\n---\n\n## 3. Frozen lockfile — regenerate and commit the lockfile in the same change\n\nCI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.\n\n**After every dependency edit, regenerate the lockfile with the repo's package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, `bun install`) and stage it with the manifest.** The pnpm case:\n\n```bash\npnpm install            # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml\ngit add package.json apps/docs/package.json pnpm-lock.yaml\n```\n\nRules:\n\n- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.\n- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.\n- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors \"lockfile not up to date,\" you forgot the plain install.\n\n---\n\n## 4. Vercel monorepo recipe (root-aware install + build)\n\nA plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/` (for a static build; see d) for `vercel()`). `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.\n\nThis is a **copyable template**, not prose. Three pieces:\n\n**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"blume dev\",\n    \"build\": \"blume build\",\n    \"preview\": \"blume preview\"\n  },\n  \"dependencies\": {\n    \"blume\": \"^2\"\n  }\n}\n```\n\n**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:\n\n```json\n{\n  \"$schema\": \"https://openapi.vercel.sh/vercel.json\",\n  \"framework\": null,\n  \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\",\n  \"buildCommand\": \"pnpm run build\",\n  \"outputDirectory\": \"dist\"\n}\n```\n\n**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**\n\n- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.\n- **Node version** = 22.19 or newer (Blume requires it). Or pin it in the docs package: `\"engines\": { \"node\": \">=22.19\" }`.\n\nHow the pieces fit, with Root Directory = `apps/docs`:\n\n- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.\n- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.\n- `outputDirectory: \"dist\"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.\n- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.\n\nIf the repo uses **Turborepo**, you can instead build through the root pipeline — `\"buildCommand\": \"cd ../.. && pnpm turbo build --filter=docs\"` — for remote-cache reuse. Keep `outputDirectory: \"dist\"` and Root Directory `apps/docs`.\n\nReport every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.\n\n**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)\n\n**d) Server output (`deployment: vercel()`).** A `vercel()` build (needed for the assistant, the MCP server, Mixedbread search, or the playground proxy) doesn't leave a static `dist/` for Vercel to serve. It writes the Build Output API folder `.vercel/output` in the docs package, which Vercel deploys as is. Use the same `vercel.json` without `outputDirectory`: `{ \"$schema\": …, \"framework\": null, \"installCommand\": \"cd ../.. && pnpm install --frozen-lockfile\", \"buildCommand\": \"pnpm run build\" }`.\n\n- Keep `framework: null`: it overrides a preset the project already has (Nuxt, Next.js), whose build settings would replace these.\n- Root Directory and Node are the same dashboard settings. Server secrets (`AI_GATEWAY_API_KEY` unless the project relies on Vercel's OIDC token) go in the project's environment variables.\n- With Turborepo, add `\".vercel/output/**\"` to the docs task's `outputs`.\n- The redirect caveat above doesn't apply: `vercel()` writes redirects into `.vercel/output/config.json`, so they're real 3xx responses.\n- `blume preview` can't serve this build; check it with `blume dev` and a preview deployment.\n\n---\n\n## 5. Astro/Vite version mismatch → pnpm patch (manual)\n\nBlume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.\n\n**Detect the risk factor** before it bites:\n\n```bash\ngrep -rn \"\\\"vite\\\"\\|vite@\\| vite:\" pnpm-workspace.yaml package.json 2>/dev/null | grep -i \"override\\|resolution\\|catalog\\|vite\"\n```\n\nBlume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):\n\n```bash\n# 1. Open the offending version in an editable temp dir (use the exact version\n#    pnpm resolved — check `pnpm why astro`):\npnpm patch astro@7.0.6\n\n# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface\n#    (the exact edit depends on the mismatch; the crash's stack trace names the\n#    file and call site). Then commit the patch:\npnpm patch-commit '<the temp path pnpm printed>'\n```\n\n`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  astro@7.0.6: patches/astro@7.0.6.patch\n```\n\nCommit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.\n\nThis is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.\n\n---\n\n## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)\n\nIf the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, titled fences like `:::warning[Heads up]`, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.\n\nThe fix is a committed **pnpm patch** (`patches/oxfmt@0.71.0.patch`), shipped with this skill at `assets/oxfmt@0.71.0.patch` — the same diff Blume's own repo applies, covering titled fences as well as bare ones. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:\n\n```bash\n# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;\n#    <skill> = this skill's directory, the one containing SKILL.md):\nmkdir -p patches\ncp \"<skill>/assets/oxfmt@0.71.0.patch\" patches/oxfmt@0.71.0.patch\n```\n\nThen register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:\n\n```yaml\n# pnpm-workspace.yaml\npatchedDependencies:\n  oxfmt@0.71.0: patches/oxfmt@0.71.0.patch\n```\n\nCommit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.\n\n- **The patch is pinned to `oxfmt@0.71.0`** (the version Blume's own repo pins). pnpm requires an exact version match, and the diff targets a file whose name is hashed per oxfmt release (`dist/markdown-*.js`), so it won't apply to any other version. If `pnpm why oxfmt` reports a different one, pin `oxfmt` to `0.71.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.59 as a peer). If the repo can't take that pin, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.\n- **The risk isn't Ultracite-only.** Any plain `oxfmt` run over Markdown (a lint-staged `oxfmt` on `**/*.md`, say) treats `:::` the same way. Check `lint-staged`/pre-commit config for globs that reach the migrated pages; `.mdx` files outside an `*.md` glob are safe.\n- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.\n\n---\n\n## Checklist for a workspace migration\n\n- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.\n- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.\n- [ ] `npmMinimalAgeGate` present? Added `blume` to `npmPreapprovedPackages` only.\n- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.\n- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22.19+ in the Vercel project.\n- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.\n- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.71.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.\n- [ ] Reported every repo-specific edit and every manual step left to the user.\n\n<!-- File: .agents/skills/blume-migrate/references/nextra.md (Tokens: ~3378 | Category: anthropic-skill) -->\n# Nextra → Blume\n\nNextra (on Next.js) declares navigation and per-page labels in `_meta` files — and crucially, **a folder's title and its pages' sidebar labels live in the _parent_ `_meta`**. Reconciling that cross-file inheritance is the main task. Nextra 4 was a near-rewrite, so **fingerprint the generation first** — it decides where config lives, where content lives, and what the components are called.\n\n## Detect & fingerprint the generation\n\n- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).\n- **v2/v3 (Pages Router):** content in `pages/`, `_meta.json` (v2) or `_meta.{js,ts}` (v3), a `theme.config.{js,jsx,ts,tsx}`, components from `nextra-theme-docs` (v2) or `nextra/components` (v3).\n- **v4 (App Router):** **no `theme.config`** — config lives as component props in `app/layout.{jsx,tsx}` (`<Layout>`, `<Navbar>`, `<Banner>`, `<Footer>`, `<Search>`). Content is either a **`content/`** directory (with a catch-all `app/[[...mdxPath]]/page.jsx` gateway) **or** Next page files: **`app/**/page.mdx`**.\n- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.\n\n**v4 app-directory repos need a restructure, not a `content.root`.** Pointing `content.root` at `app/` gives every route a trailing `/page` and scans `layout.jsx`/gateway files. Move each `app/foo/page.mdx` → `foo.mdx` (or `foo/index.mdx` when it has children), and exclude the non-MDX app files. The catch-all `page.jsx`, `mdx-components.*`, and `app/layout.*` are teardown artifacts — but **harvest `app/layout.*` first** (see Config).\n\n## Config\n\nNone of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:\n\n- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the footer's repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`; if its origin is not `https://github.com`, set `github.host` to that origin — with `host` omitted, Blume builds the repository and edit links against public GitHub); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer.text` (v2) / `footer.content` (v3) → `footer.copyright` when it's a copyright line: reduce the JSX to plain text, and a `{new Date().getFullYear()}` becomes `${new Date().getFullYear()}` in a template literal; anything else in it → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.\n- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>` (its children are v3's `footer.content`). Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).\n- **`next.config.*`:** harvest **`redirects()`** — static entries and `:param`/`:path*` patterns become Blume `redirects: [{ from, to }]` as written (`permanent: true` → `status: 308`, `false` → `307`); ones with regex params (`:slug(\\d+)`) or `has`/`missing` conditions move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.\n- Root `_meta` entries with **`type: \"page\"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: \"page\"` only maps at the **root**.\n\n## Navigation: `_meta` → `meta.ts` + frontmatter\n\n**Every `_meta` file must be carried over — always convert it to `meta.ts` + frontmatter, never drop it in favor of filename inference.** `_meta` is Nextra's canonical nav source (ordering, labels, folder titles); the filesystem alone can't reproduce it.\n\nFor each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):\n\n| Nextra `_meta` entry | Blume |\n| --- | --- |\n| ordinary page/folder (string or `{ title }`) | slug → parent `meta.ts` `pages` (ordering). A **page** title → that page's frontmatter `sidebar.label`. A **folder** title → that child folder's `meta.ts` `title` (title lives in the parent!). A JSX title → extract its text. |\n| `display: \"hidden\"` | frontmatter `sidebar.hidden: true` |\n| `type: \"separator\"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |\n| `type: \"menu\"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |\n| `href` (external link) | root-level → **`navigation.actions`** (`[{ label, href }]`, plain header links, matching Nextra's navbar placement) or **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route, survives on phones); only drop deep-nested ones (report) |\n| `type: \"page\"` (subfolder, not root) | drop (only root → tabs) |\n| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |\n| `theme: { layout: \"full\" }` on a page | that page's frontmatter `mode: wide` (full column width, no table of contents — the closest match; report) |\n| other `theme` keys (`sidebar: false`, `toc: false`, `typesetting`, …) | drop (report — no per-page switches for them) |\n| `\"*\"` wildcard entry | apply its value as the default for unlisted siblings, then drop |\n\nWrite each folder's `meta.ts` with its `pages` order (from its own `_meta`) and its `title` (inherited from the parent's `_meta`). A v4 **`_meta.global.*`** file holds the whole tree in one place (folder entries carry `items`) — split it into per-folder `meta.ts` files with the same rules.\n\n**Keep the order with `display: \"group\"`.** Nextra draws folders as collapsible rows and keeps pages and folders interleaved as `_meta` lists them. Blume's default `flat` display draws a folder as a plain section header, so it lists each level's pages above its subfolders — a page `_meta` puts after a folder jumps above it. Set `navigation.sidebar.display: \"group\"` (or `display: \"group\"` in the `meta.ts` of every subfolder at a level whose order must hold — one flat sibling folder reorders the level) to keep the authored order in nested folders. Groups then start closed unless they hold the current page, while Nextra opens every folder shallower than its `defaultMenuCollapseLevel` (default `2`: the top-level folders), so add `collapsed: false` to those folders' `meta.ts`. The top level (and each tab's top level) lists loose pages before folders in every display mode, so a root `_meta` that puts a page after a folder can't be matched — report it.\n\n## Frontmatter & titles — synthesize, don't just pass through\n\nMost Nextra pages have **no frontmatter**. Nextra keeps two titles apart: the **sidebar label** falls back `_meta` title → `sidebarTitle` → `title` → first body H1 → filename, while the **heading readers see** is the body H1. Blume renders frontmatter `title` as the page H1, so for **every page**: set frontmatter `title` from the body H1 (else frontmatter `title`, else the `_meta` title, else a humanized filename), **remove the body H1**, and set `sidebar.label` to the `_meta` title (else `sidebarTitle`) wherever it differs from `title`. Don't take the `_meta` label as `title` — that renames the page's heading. `asIndexPage: true` (v4 folder-index marker) → make the file the folder's `index.mdx` and drop the key. Drop any other non-schema key and report it.\n\n## Components\n\n- **Callouts:** `<Callout type=\"x\">` → directive. Types: `default` (the prop default) → `:::note`, `info`→`:::info`, `warning`→`:::warning`, `error`→`:::danger`, `important` (v4) → `:::note`. Bare `<Callout>` → `:::note`. Drop `emoji` (there is no `title` prop to carry — Nextra callouts have none).\n- **v2 flat names:** v2 exported `Tab` and `Card` (flat) from **`nextra-theme-docs`**; v3/v4 use `Tabs.Tab`/`Cards.Card` from `nextra/components`. Handle both: `<Cards>`/`<Cards.Card>`/`<Card>` → `<CardGroup>`/`<Card>`; `<Tabs items={[…]}>` + `<Tabs.Tab>`/`<Tab>` → `<Tabs>`/`<Tab title=\"…\">` (move labels from the parent `items` onto each `<Tab>`). `Cards` `num` → `CardGroup` `cols`.\n- **`<Steps>`:** Nextra's wraps **Markdown headings (h2–h6)**, one step per heading. Convert each heading to a `<Step title=\"…\">` child (Blume `<Steps>`/`<Step>`) and delete the heading — this also keeps step titles out of the TOC, matching Nextra's behavior.\n- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).\n- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.\n- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` pass through in `.mdx`, where Blume renders them as note/tip/warning/note/danger callouts. In a `.md` page they stay plain quotes (`BLUME_MD_GITHUB_ALERT`): rename it to `.mdx`.\n- **Strip or convert every import:** `nextra`, `nextra/*`, `nextra-theme-docs`, `nextra-theme-blog`, plus `next/image` (→ Markdown image or `<Frame>`), `next/link` (→ plain link), and local components (port or inline; report).\n\n## Code fences\n\nNextra's fence meta differs from Blume's — rewrite it: `filename=\"app.js\"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags either one left); line highlighting `{1,4-5}` carries over unchanged; **drop** word-highlight `/word/` and `copy`/`copy=false`. **Keep** inline-code `{:lang}` suffixes (`` `useState(){:js}` ``) — Blume highlights them natively. ` ```sh npm2yarn ` fences → ` ```package-install `.\n\n## Math\n\nNextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume, block math `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field). Convert Nextra's ` ```math ` fences → `$$…$$` blocks (they render as plain code blocks otherwise). Inline math is `$$…$$` too: a single `$` stays literal, so convert each inline `$…$` to `$$…$$` inside its sentence. MathJax-specific macros → report.\n\n## i18n\n\n- **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → match Blume's `dot` parser as-is, default-locale suffix included: `i18n: { defaultLocale, locales: [{ code, label }], parser: \"dot\" }`, no file moves. Page titles land in each file's own frontmatter, so they stay per-locale, but under `dot` a folder's `meta.ts` serves every locale — build it from the default locale's `_meta` and report translated folder titles from the other locales' `_meta` files.\n- **v4:** every locale has a `content/<lang>/` dir, the default included, and Nextra serves every locale under its prefix (`/en/…`). Blume's `dir` parser takes the other locales' dirs as-is, but the default locale lives **at the content root**: a `content/en/` dir is ordinary content that publishes at `/en/…` and warns `BLUME_I18N_DEFAULT_LOCALE_FOLDER`. Move the default locale's files (and `_meta`-derived `meta.ts` files) up one level, and set `i18n.hideDefaultLocalePrefix: false` to keep serving them at `/en/…` as Nextra did — or keep Blume's unprefixed default and add a `redirects` entry per page.\n\n## Package.json & teardown\n\nRepoint `dev`/`build`/`start` scripts to the Blume CLI; remove `next`/`nextra`/`nextra-theme-*` deps and add `blume`. Delete after harvesting: `next.config.*` (redirects first!), `theme.config.*` (v2/v3), `app/layout.*` + the catch-all gateway + `mdx-components.*` (v4).\n\n## Dropped — report these\n\nFooter content other than a copyright line; `primaryHue`-style theming beyond an accent color; `faviconGlyph`; `_meta` separators, menus, `newWindow`; per-page `theme` switches (and `layout: \"full\"` approximated as `mode: wide`); root-level pages `_meta` ordered after a folder; `<Bleed>`; MathJax macros; word-highlight/copy fence meta; any icon you can't reconstruct as a Lucide name.\n\n<!-- File: .agents/skills/blume-migrate/references/readme.md (Tokens: ~9064 | Category: anthropic-skill) -->\n# ReadMe → Blume\n\nReadMe (readme.com developer hubs) is a hosted platform. Only content reaches Git: settings, the landing page, the glossary, variables, redirects, and every metrics and login feature live in its dashboard. Pages are MDX, or ReadMe's looser \"MDXish\" dialect, saved as `.md`. Published URLs are flat per section (`/docs/<slug>`, `/reference/<slug>`, `/recipes/<slug>`, `/page/<slug>`, `/changelog/<slug>`) however deep a page sits in the sidebar. The plan below keeps every one of them: section folders stay at the top of the content root, categories become `(group)` folders, and nested pages pin their flat URL with `slug`. The API reference is the exception: Blume generates operation pages at new routes, so each old one gets a redirect.\n\nTwo things go wrong with a green build and no warning: emoji blockquote callouts (plain blockquotes) and `:emoji:` shortcodes (literal text). Two more only warn: `doc:`/`ref:` links (dead links; `blume validate` reports `BLUME_UNSUPPORTED_LINK_SCHEME`) and inline `onClick` handlers (they render and do nothing; `BLUME_MDX_EVENT_HANDLER`). The codemod handles the first three; the handlers are yours.\n\n## Detect\n\n| Shape | Tells | The sidebar lives in |\n| --- | --- | --- |\n| **A. Bi-directional Git sync** (ReadMe Refactored) | `_order.yaml` in `docs/`, `reference/`, `recipes/`, `custom_pages/` and their folders; category folders with spaces (`docs/Getting Started/`); `reference/*.json` specs beside endpoint pages with `api: { file, operationId }` frontmatter; `reference/ReadMeConfig/`; `custom_blocks/`; a `main` branch holding only `changelogs/`; branches named after versions (`v3.0`) | folders and `_order.yaml` |\n| **B. `rdme@10` upload** (one-way) | CI running `readmeio/rdme@v10` or `npx rdme@10` with `docs upload`, `reference upload`, `changelog upload`, `custompages upload`, `openapi upload`; frontmatter `category: { uri }`, `parent: { uri }`, `position`, `privacy: { view }`, `content: { excerpt }` (also `rdme docs export` output) | frontmatter |\n| **C. Legacy `rdme` ≤ 9** | CI running `readmeio/rdme@v9` or older (`rdme docs ./dir`, `rdme openapi`, `rdme changelogs`); frontmatter `category: <24-hex id>` or `categorySlug`, `parentDoc` or `parentDocSlug`, `order`, `hidden`, `type: basic \\| link \\| error` | frontmatter, keyed by opaque IDs |\n\nrdme ignores folder layout, so in shapes B and C the folders say nothing about the sidebar. rdme 10 still accepts `rdme changelogs` as a hidden alias, so tell B from C by the rdme version and the frontmatter. A shape A repo can still run `rdme openapi upload` in CI: it's shape A.\n\n**Get everything before you convert.**\n\n- **Shape A** is complete per version branch. Find the branch holding the hub's default version (it can be `v3.0`, not `main`). **Changelogs live only on `main`**: `git archive <main-sha> changelogs | tar -x -C <work-dir>`. Ignore `<version>_<name>` branches (ReadMe's editing branches, kept after they merge).\n- **Shapes B and C** hold only what CI uploads, and drift: editor changes never come back, and a failing upload step stops changes going out. Inventory the live hub, export what the repo lacks, and diff pages against their live source.\n- **Exports.** A Refactored project: connect bi-directional sync to a new, empty repository (Settings → Git Connection), which writes shape A with every version as a branch; a ZIP from the Branch menu (no images); or `npx rdme@10 docs export <dir> --branch <version>` and `rdme reference export <dir>` (shape B frontmatter). A legacy project: `rdme@9` can't export; read pages through API v1 (`GET /api/v1/categories/{slug}/docs`, `GET /api/v1/docs/{slug}`) or upgrade the project. Refactored hubs usually serve a page's Markdown at `<url>.md` (docs.readme.com does), and `/llms.txt`, when it's on, lists every public page or links per-section files that do.\n\n**Go easy on the live hub.** A ReadMe hub can start answering `429` after as few as eight requests in a few minutes, even spaced seconds apart. Spend them on one page of the hub (save it: it carries the settings JSON below), `sitemap.xml`, `llms.txt`, and a few spot checks. The `files.readme.io` image CDN is separate: one run fetched 84 images there at one every half second without a `429`.\n\n## Run the codemod first\n\nFor shape A, on a branch whose name matches no ReadMe version (see Teardown), with the changelogs copied from `main` into the work tree and one page of the hub saved as `hub.html` (skip the `curl` when the user already saved one at the root):\n\n```bash\ncurl -s https://docs.example.com/docs/<any-page> -o hub.html   # settings JSON + glossary\nnode <skill>/scripts/readme-codemod.mjs --site https://docs.example.com --glossary hub.html .          # report only\nnode <skill>/scripts/readme-codemod.mjs --write --site https://docs.example.com --glossary hub.html .  # apply\n```\n\n`--site` (repeatable: add the `*.readme.io` origin too) makes absolute links to the hub root-relative. It's zero-dependency, deterministic, and idempotent (a rerun reports `Nothing to change.`). It:\n\n- moves `custom_pages/` → `page/`, `changelogs/` → `changelog/`, categories → `(Category)` group folders, `.md` → `.mdx`;\n- maps frontmatter (Frontmatter, below) and pins `slug` wherever Blume's route would differ from ReadMe's URL;\n- converts callouts, links, magic blocks, ReadMe components, variables, reusable content, icons, shortcodes, code fences, and headings (Content);\n- writes `_order.yaml` → `meta.ts`, deletes endpoint, tag, and empty parent pages, and turns link pages into redirects (Navigation);\n- writes `reference/overlays/<spec>.overlay.json` (API reference), `readme-redirects.json` (the redirects it can compute), and `readme-migration.json` (every old URL, every deleted endpoint, for the second pass).\n\n**Its report is the to-do list**: per file, what needs judgment (custom components, inline components, `<style>`, event handlers, dead links, unmapped icons), then \"Left for you\". Work through it with the sections below. A second pass after the first build handles endpoint redirects and order (API reference).\n\nShapes B and C: rebuild the tree first (Navigation, shapes B and C), then run the codemod. With no `_order.yaml` it converts page content and frontmatter only: no moves, slug pins, URL inventory, endpoint handling, or redirects (it reports link pages instead).\n\n## Config: the dashboard → `blume.config.ts`\n\nThere's no config file in Git. Every page of a Refactored hub embeds the project's settings as JSON: read them from the saved `hub.html` rather than asking. Map what's set:\n\n| ReadMe (settings JSON) | Blume | Notes |\n| --- | --- | --- |\n| `name` | `title` |  |\n| `logo`, `logo_white_use` | `logo` | download into `public/`. With `logo_white_use: true` the hub showed a white logo on a colored header, invisible in Blume's light header: recolor the white parts to `currentColor`. A bare mark → the string form (`logo: \"/logo.svg\"`); a wordmark → `{ image: \"/logo.svg\", text: \"\" }` (SKILL.md) |\n| `favicon` | `public/favicon.png` (or `.svg`/`.ico`) | picked up by name |\n| `brand.link_color` (`colors.body_highlight`) | `theme.accent` | `_dark` variants → `{ light, dark }` |\n| `brand.primary_color` (`colors.main`) | `theme.action`, or `theme.accent` when no link color | the colored header has no equivalent: report |\n| `brand.theme` | `theme.mode` |  |\n| `typography.headline` / `body` (`Open+Sans:400:sans-serif`) | `theme.fonts.display` / `body` (`{ name: \"Open Sans\" }`) | Typekit → local `variants`, or drop |\n| `nav_names` + enabled `modules` | `navigation.tabs` | one tab per enabled section with content (`{ label: \"Developer hub\", path: \"/docs\" }`) |\n| header links (`link_url` entries) | `navigation.actions` | a sign-up or \"Get API keys\" button → `navigation.cta`; a login link stays in `actions` |\n| `first_page` | `index.mdx` at the root, or a `/` redirect (status 302) | the landing page doesn't sync |\n| `custom_domain` | `deployment.site` | unless the host is Vercel or Netlify (SKILL.md) |\n| Enterprise group path (`/<project>/docs/…`) | `basePath: \"/<project>\"` | one Blume site per child project |\n| `integrations.google.analytics`, `segment`, `heap`; custom JS | `googleAnalytics()`, `segment()`, `heap()`, or `script({ src \\| content })` in `analytics` | these load only in production builds, and wait for `consent` when it's set |\n| `integrations.intercom`, `zendesk` (a support widget readers click) | a `PageFooter` component (`defineComponents`, SKILL.md \"analytics\") that loads it | not `analytics`, which can hold it until consent. Check region settings (an AU Intercom workspace needs `api_base`) |\n| `integrations.google.site_verification`, `<meta>` tags in head HTML | `seo.metatags` | other head HTML: report |\n| `stylesheet_hub2` (custom CSS) | a root `theme.css` | rules for ReadMe's classes (`.rm-*`, `.callout_*`, hashed names) match nothing: carry only rules for the site's own markup |\n| Footer HTML | `footer: { copyright, links }` | a copyright line → `copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`). Report the rest |\n| `variables.defaults` | `variables` | Content. **Defaults can be real credentials** (an API key): never copy one into config |\n| `glossaryTerms` | `--glossary hub.html` | the codemod writes `<Tooltip>`s |\n| `redirects` | `redirects` | URLs and redirects |\n| `error404` | `pages/404.astro` or Blume's default | URLs and redirects |\n| Ask AI, MCP server | `ai.assistant`, `agents.mcp` | need a server-output host adapter: report |\n\nThen the layout. **Leave the section folders where they are.** Set `content.root: \".\"` and scope `include` (`references/monorepo.md` §1), import the codemod's redirects, and add the reference:\n\n```ts\nimport { defineConfig } from \"blume\";\nimport { openapi } from \"blume/reference\";\nimport redirects from \"./readme-redirects.json\";\n\nexport default defineConfig({\n  content: {\n    root: \".\",\n    include: [\n      \"index.mdx\",\n      \"docs/**/*.mdx\",\n      \"reference/**/*.mdx\",\n      \"recipes/**/*.mdx\",\n      \"page/**/*.mdx\",\n      \"changelog/**/*.mdx\",\n    ],\n  },\n  redirects: [...redirects /* , your own */],\n  reference: [openapi({ sources: [/* API reference */] })],\n});\n```\n\n`page/` is ReadMe's custom pages at `/page/<slug>`; Blume's own `pages/` folder is for `.astro` pages, a different thing.\n\n## Navigation (shape A)\n\nWhat the codemod does, so you can check it:\n\n- **Categories → group folders.** `docs/Getting Started/` → `docs/(Getting Started)/`: the group keeps the label and adds no URL segment.\n- **`_order.yaml` → `meta.ts` `pages`**, entries as written (`pages` ignores the parentheses), less duplicates and entries with no file. A category folder's `meta.ts` also sets `title` where Blume would relabel the folder name (`Self-Serve` would show as \"Self Serve\"). A parent page's folder gets `title` from the parent and `display: \"group\"`.\n- **`slug` pins.** ReadMe serves every child flat (`/docs/<child>`; the nested path 404s), so every page below a parent folder gets `slug: docs/<stem>`, and a nested parent's own `index.mdx` takes `docs/<folder>`. A parent folder directly in a category needs none: its folder name is the route. Any stem starting with a digit gets one too, since Blume strips `^\\d+[-_.]` as an ordering prefix (`1-setup` → `/docs/setup`); dates (`12-05-2022`, `2024-01`, `2022-05-12`) and versions (`1.2`) stay whole, so their pins are harmless.\n- **Hidden pages** (`hidden: true`) were left out of the sidebar and search but served by link: `hidden: true` + `search.exclude: true`, URL kept.\n- **Empty parent pages** (frontmatter, no body) redirected to their first child on ReadMe. The codemod deletes them and adds a 302 to the first child; prefer that to keeping one as a `directory: \"card\"` landing page. A parent that stays (with a body, or kept as a landing page) whose child has the same title fails `validate --strict` (`BLUME_NAV_DUPLICATE_LABEL`): retitle one, and report it.\n- **Link pages** (`link: { url }`): deleted, with a 302 to the URL. If the link must stay visible, add it to `navigation.featured`.\n- **Section roots.** `/docs` and `/reference` redirect to the section's first page, as on ReadMe; a typed URL would 404 without it. ReadMe answers them with a 301; the codemod writes a 302, since the first page can change.\n- **Custom pages** had no sidebar: `hidden: true` plus `mode: center` (`fullscreen: true` → `mode: custom`).\n\nLeft for you: an HTML custom page (`custom_pages/<slug>.html`) → `page/<slug>.mdx` with its HTML as JSX, or `pages/page/<slug>.astro` on `PageLayout` for a full document with scripts (`docs/advanced/custom-pages.mdx`); and `/` (see `first_page`).\n\n### Versions\n\nVersion branches hold full copies of guides, recipes, and the reference; ReadMe serves the default version without a prefix and others at `/<version>/docs/<slug>`. **Recommend migrating the default version only**, with `{ from: \"/<version>/:path*\", to: \"/:path*\" }` for each other version and the default's own prefix (`/v3.0/docs/x` redirected to `/docs/x` on ReadMe). Pages that only exist in an old version then 404: report it. To keep old versions, run the codemod on each branch's checkout and copy its `docs/`, `recipes/`, and `reference/` into `<version>/` (`v2.1/docs/…` serves `/v2.1/docs/…`), drop that version's prefix redirect (a pattern over pages fails the build), add `\"<version>/**/*.mdx\"` to `content.include` (a scoped `include` never scans the copy otherwise), and set `versions.archived` (newest first) and `versions.current`. Each id is the URL segment (`v2.1`): ids must start with a letter. References, the changelog, and custom pages stay unversioned, and an archived tree's sidebar isn't scoped by tabs: report both.\n\n## Navigation (shapes B and C)\n\nRebuild the tree from frontmatter into shape A's layout, then run the codemod:\n\n- **Category** → `docs/(<Category title>)/`. Shape B's `category.uri` ends in the title (`/branches/3.0/categories/guides/Getting%20Started`): decode it. Shape C's `category` is an ID: resolve it with `GET /api/v1/categories`, the hub's sidebar, or the user. `categorySlug` names it.\n- **Parent** (`parent.uri`, `parentDoc`, `parentDocSlug`) → a folder named for the parent's slug, holding the parent as `index.mdx`; pin each child's `slug` as in shape A.\n- **Order** (`position`, `order`) → the folder's `meta.ts` `pages`.\n- **Slug**: frontmatter `slug`, else the file name. Name each file after its slug, and pin any that starts with a digit.\n- **Custom pages and changelog entries** → `page/<slug>.md` and `changelog/<slug>.md`, whatever folder CI uploaded them from: the codemod maps their frontmatter by those folder names (`custom_pages/` and `changelogs/` work too), but outside shape A it moves nothing, so the folder is the URL.\n\nThe codemod reports these navigation keys and leaves them: delete them once the tree is built.\n\n## Content\n\n**Every page becomes `.mdx`**: ReadMe's `.md` files are MDX, and in a Blume `.md` page directives stay literal text and components don't render. The codemod converts the mechanical syntax (table below). It also clears what ReadMe's lenient MDXish engine (`\"mdxish\": true` in the settings JSON) accepts and strict MDX rejects: it deletes `<!-- -->` comments, self-closes void tags, and converts `[block:…]` JSON. **It leaves everything that needs judgment, and reports it per file:**\n\n- **Inline components.** A page that defines a component (`export const Name = …`) is left byte for byte. Without hooks it renders as static HTML. With hooks it fails the build: move it to `islands/<Name>.tsx` (`export default`). One defined on several pages goes in `islands/` once.\n- **Custom components** (`custom_blocks/<Name>.mdx`): use a Blume component when it's presentational (a card grid → `CardGroup`), else move it to `islands/<Name>.tsx` and keep `<Name />` in the pages. Blume compiles Tailwind v4 utilities from the project's `.mdx` and `.tsx` files (Tailwind's default theme plus Blume's tokens, `dark:` keyed to Blume's theme), so utility classes keep working; ReadMe's CSS variables (`var(--color-border-default)`) don't exist in Blume: swap them for Blume's (`var(--blume-border)`) or its token utilities (`border-border`, `bg-muted`). Text moved from a prop into children needs `{` and `}` escaped (`\\{`), or the build fails (`BLUME_MDX_UNDEFINED_NAME`, at the brace's line).\n- **Event handlers.** `<button onClick={…}>`, `onMouseEnter`, and friends on inline HTML build green and do nothing; `blume check` and `build` warn `BLUME_MDX_EVENT_HANDLER` at each one. Rebuild the widget as an island (`grep -rnE 'on[A-Z][a-zA-Z]*=' --include='*.mdx'` outside code finds them before a build). Hubs often paste the same widget (a \"contact support\" box) into dozens of pages: one island, and replace every copy. A handler that calls a support widget's global (`Intercom('show')`) also needs that widget loaded (Config, `integrations.intercom`).\n- **`<HTMLBlock>` with `<style>`, `<script>`, or handlers** stays wrapped. **Don't move its `<style>` rules into `theme.css`**: ReadMe authors write global selectors (`p`, `ul`, `h3, h4`), which there restyle the whole site, sidebar included. Rebuild the markup with Blume components (`Tabs`, `Columns`, `CardGroup`, `Panel`, callouts). ReadMe stripped `<script>` unless `runScripts` was on: rebuild what a script did as an island or a `script()` adapter.\n- **Font Awesome `<i>` icons** in raw HTML (`<i className=\"fa fa-plug\" />`): `<Icon icon=\"<lucide>\" />`; Blume loads no Font Awesome, so the `<i>` renders empty. Tailwind classes in raw HTML (`className=\"mt-4 p-4 bg-gray-50\"`) still compile, but fixed colors ignore dark mode: prefer a component (`Panel`, a callout) or Blume's token utilities (`bg-muted`, `border-border`).\n- **Unknown components** (`<PostmanRunButton>`, `<MCPIntro>`, `<TableOfContents>`): drop or replace; each fails the build.\n- **Variables.** `<<name>>`, `{user.name}`, and `<Variable name>` became `{{name}}`; define each in `variables` (from `variables.defaults`). ReadMe filled them per reader; Blume shows one default to everyone (report). The codemod leaves `<<name>>` inside code as written and reports it: change one to `{{name}}` by hand where it should show the default (Blume substitutes in code too).\n- **Glossary terms** with no definition: pass `--glossary`, or write the `<Tooltip>` by hand.\n- **Dead links.** Hubs collect links to slugs from older versions that 404 on the live site too. Check a few live first (ReadMe cross-redirects some, `/reference/x` → `/docs/x`), then point each at the closest current page, or unlink it and keep the text. Report the list.\n- **Images** stay on `files.readme.io`: download them into `public/` and rewrite the URLs (`grep -rhoE 'https://files\\.readme\\.io/[^)\" ]+'`). Some have no extension: add one. Review every alt the codemod derived from a file name.\n- **Tables** with lists or blocks in their cells, or spanning cells (`colSpan`, `rowSpan`), stay `<table>`: check they render.\n- **Recipes**: the codemod deletes the line-highlight comments and demotes the step headings; the walkthrough modal is dropped (report). Repeat a highlighted excerpt under its step with `{1-4}` where it mattered.\n\n| ReadMe | Blume (codemod) |\n| --- | --- |\n| `> 📘 Title` / `>` / `> Body` (emoji first) | `:::info[Title]` … `:::`; 📘 ℹ️ → `info`, 👍 ✅ → `success`, 🚧 ⚠️ → `warning`, ❗️ 🛑 ‼️ ⁉️ → `danger`, any other emoji → `note`. An emoji alone means no title; `#` marks drop |\n| `<Callout icon theme>` | the directive for `theme` (`okay` → success, `warn` → warning, `error` → danger, `default` → note), else for `icon`; a first-child heading becomes the title |\n| `[block:callout]`, `[block:code]`, `[block:image]`, `[block:table]`/`parameters`, `[block:api-header]`, `[block:embed]`, `[block:html]` | the same conversions as their component forms |\n| adjacent fences (ReadMe's code tabs) | `<CodeGroup>`; untitled tabs labeled with the language, as ReadMe showed them |\n| ` ```javascript Node SDK ` | ` ```javascript title=\"Node SDK\" `; `node` → `js`, `curl` → `bash`, `cplusplus` → `cpp`, `objectivec` → `objc` |\n| `<Image src alt caption>`, `<Image …>caption</Image>`, `![1544](url \"image.png\")` | `![alt](src)`, in `<Frame caption>` when captioned; numeric or missing alts derived from the file name; `align`/`width`/`border` drop |\n| `<Embed>` (YouTube, including `google.com/sorry` interstitials), `[x](url \"@embed\")` | `<YouTube id>`; anything else → a link |\n| `<HTMLBlock>{`…`}</HTMLBlock>` | its HTML, inline: comments removed, void tags closed, braces as entities |\n| `<Table>` | a Markdown table: a cell's paragraphs joined with `<br />`, all-empty rows dropped |\n| `<Cards columns>` / `<Card iconColor badge target>` | `<CardGroup cols>` / `<Card color>`, `badge` → `<Badge>`, `target` drops; braces from props escaped |\n| `<Columns layout>`, `<Tab icon>` | `<Columns cols>`, Lucide icons |\n| `<Accordion title>` | a run → one `<Accordion>` of `<AccordionItem>`s; one alone → `<Expandable>`, or a one-item `<Accordion>` when it has an icon (`Expandable` takes none) |\n| `<Glossary>`, `<<glossary:x>>` | `<Tooltip tip>` |\n| `<Anchor>`, `<Recipe>`, `[block:recipe]`, `[block:tutorial-tile]` | a link, `<Card href=\"/recipes/<slug>\">` |\n| `custom_blocks/<Name>.md` used as `<Name />` | `_snippets/<name>.mdx` + `<include>/_snippets/<name>.mdx</include>` |\n| `doc:x`, `ref:x`, `page:x`, `changelog:x`, `blog:x`; `https://<site>/docs/x` | `/docs/x`, `/reference/x`, …; root-relative |\n| Font Awesome (`fad fa-hand-wave`) on frontmatter, cards, tabs, accordions | Lucide, or dropped and reported |\n| `:tada:` | 🎉 (unknown names reported) |\n| a body `# H1` repeating the title; any other body H1 | deleted; every body heading demoted one level |\n\nCallout titles had anchor ids on ReadMe; Blume's don't. Headings otherwise keep their anchors: both slug with github-slugger, so skip `pin-heading-ids.mjs`, which would also crawl the rate-limited hub. One exception: ReadMe's slugger also counted callout titles and the H1 the codemod deletes, so a later heading with the same text was `<slug>-1` on ReadMe and is `<slug>` in Blume. Another: for a heading that ends in an emoji or symbol after a space, github-slugger keeps a trailing dash (`## Features ✨` → `#features-`), which Blume drops (`#features`). `blume validate --strict` flags links to the old id; retarget them.\n\n## Frontmatter\n\nBlume's page schema is strict. The codemod maps:\n\n| ReadMe | Blume |\n| --- | --- |\n| `title` | `title` |\n| `excerpt` (A, C), `content.excerpt` (B) | `description` |\n| `hidden: true` (A), `privacy.view: anyone_with_link` (B) | `hidden: true` + `search.exclude: true` |\n| `hidden: true` (C) | the same, reported: rdme maps it to `privacy.view: anyone_with_link`, but check the live URL and use `draft: true` if it 404s |\n| `deprecated`, `state: deprecated` (B) | `deprecated: true` |\n| `icon`, `appearance.icon` (B) | `icon` (Lucide) |\n| `metadata.title` / `description` / `image` | `seo.title` / `description` / `image`; B's `image: { uri }` is an image id, not a URL: reported |\n| `metadata.keywords` | `search.keywords` |\n| `metadata.robots: noindex`, `allow_crawlers: disabled` (B) | `noindex: true` |\n| `next.pages`, `content.next` (B) | `related`, dropping entries that aren't pages in the repo; Blume takes 10 at most (the rest reported) |\n| `link`, `content.link` (B) | a redirect (Navigation) |\n| `table_of_contents: false` | `mode: wide` (an approximation: report) |\n| `fullscreen` (custom pages) | `mode: custom`, else `mode: center` |\n| `recipe`, `x-import`, `name`, `api`, `api_config`, `recipes` | dropped (`recipes` reported) |\n\nIt keeps keys it doesn't know and reports them: Blume rejects unknown keys, so map or delete each. It drops C's `type` (`basic`, `link`, `error`), since Blume's own `type` sets the content type. Changelog keys are under Changelog.\n\n## API reference\n\nEvery endpoint page is generated from an OpenAPI file. The codemod records each one's old URL, operation, and prose in `readme-migration.json`, and deletes it.\n\n1. **Specs → sources.** One `openapi()` source per spec in `reference/`, labeled with the ReadMe category its endpoints sat under, or the spec's `info.title`: `{ label: \"Connect\", spec: \"./reference/connect.json\" }` serves `/reference/connect`, with operations at `/reference/connect/<tag>/<operation>`. **A category that held several specs** (the report lists them) can't share a label: they'd share a route and the first would win. Give each its own label, or merge the specs first, and report the split. A single spec → `openapi({ spec })`. Add a `navigation.tabs` entry for `/reference`. A YAML spec: convert it to JSON (`npx @redocly/cli bundle x.yaml --output x.json`) and rerun the codemod, which reads JSON only.\n2. **Overlays.** `reference/overlays/<spec>.overlay.json` sets each operation's `description` to its spec text plus the prose its ReadMe endpoint page showed, and converts the ReadMe Markdown inside spec descriptions (emoji callouts, shortcodes, `doc:` links), which ReadMe rendered and Blume would show plain. Add each to its source: `{ label, spec, overlays: [\"./reference/overlays/connect.overlay.json\"] }`. JSX doesn't render in descriptions (`<` and `{` are escaped): rewrite any the report names as Markdown.\n3. **Build, then run the second pass.** List the generated routes, then hand them to the codemod with the same `--spec` pairs (the source's route, then its spec):\n\n   ```bash\n   npx blume build\n   node <skill>/scripts/operation-routes.mjs dist --spec /reference/connect=reference/connect.json > routes.json\n   node <skill>/scripts/readme-codemod.mjs --routes routes.json --spec /reference/connect=reference/connect.json --write .\n   ```\n\n   It adds a 301 from each old endpoint URL (`/reference/getaccount`) to its operation page, and a 302 from each tag page (`/reference/accounts-1`) to the tag's first endpoint, as ReadMe answered. It skips an old URL that is now a page (`/reference/enrich`, ReadMe's operation, against an Enrich source's overview): a redirect there would hide the page (`BLUME_REDIRECT_MATCHES_PAGE`). It reports each, so you can pick another `route` instead. It adds a 302 from `/reference` to the first endpoint when nothing else claims it, and drops any pass-one redirect that would now hide a reference page. It also rewrites links to old endpoint and tag URLs outside fenced code, dropping anchors (reported), and writes `<route>/<tag-slug>/meta.ts` (`defineMeta({ title, order, pages })`) with ReadMe's endpoint order, where it can differ from the spec's order, which the generated sidebar follows.\n\n4. **Source order, by hand.** Write `<route>/meta.ts` → `defineMeta({ order: <n> })` for each source, with the order the report gives (ReadMe's category order). It merges over the generated group's settings, so the source's `label` stays its title.\n\n- **Conceptual pages** (no `api` key) stay in `reference/(<Category>)/` at `/reference/<slug>` and join the reference tab's sidebar; their group folder's `meta.ts` takes an `order` among the sources.\n- **`reference/ReadMeConfig/`** (`api_config` pages: getting started, authentication, my requests) showed the reader's own API key and request log. The codemod keeps any prose as a page and deletes empty ones: redirect each deleted URL to the closest page.\n- **Hidden endpoints**: remove the operation with an overlay action (`remove: true`) if it should stay unpublished. `x-internal: true` likewise: Blume ignores the flag.\n- **`x-readme` extensions** (and flat spellings like `x-samples-languages`, at the root or on an operation): `samples-languages` → `codeSamples`: `shell`, `node`, `javascript`, `python`, `ruby`, `go`, `php`, `java`, `csharp`, `kotlin`, `swift`, `c`, `cplusplus` (read as `cpp`), and `powershell` carry over as written (so does `curl`, which some specs use); `objectivec`, `http`, `json`, `clojure`, `ocaml`, and `r` have no Blume generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: leave them out and report them. `code-samples` → `x-codeSamples` by overlay (`language` → `lang`, `code` → `source`, `name` → `label`). `explorer-enabled: false` → `playground: false`; `proxy-enabled` (on by default: Try It went through ReadMe's CORS proxy) → `playground: { proxy: true }` on a server-output host, or the API must allow the docs origin (say which). `codeSamples` and `playground` are per `openapi()` adapter, not per source or operation: when specs or operations disagree, take the setting most of them use and report the rest. `samples-enabled` is no longer supported on ReadMe (it hid nothing): ignore it. `headers`, `x-default`, `oauth-options`, `parameter-ordering`, `metrics-enabled`: no equivalent (report).\n- **Versioned references**: unversioned in Blume; an archived version's spec can be its own source with `route: \"/<version>/reference\"` (report).\n\n## Changelog\n\nThe codemod moves `changelogs/<slug>.md` → `changelog/<slug>.mdx` (same URL; digit-led stems pinned with `slug`, which date names no longer need) and adds `type: changelog`:\n\n| ReadMe | Blume |\n| --- | --- |\n| `date`, `published_at` (what synced repos contain), `created_at` (B) | `date` (the day). None (common in C) → reported: take it from the live entry |\n| `author` | `authors`; B's ID object drops |\n| `type` (`added`, `fixed`, `improved`, `deprecated`, `removed`; `none`) | `changelog.category`, capitalized; `none` omitted |\n| `hidden: true`, `privacy.view: anyone_with_link` | `hidden: true`: still built, out of the index and feed |\n\nIt also adds `/changelog.rss` → `/changelog/rss.xml`; the feed needs `deployment.site` or a detected host. Add a `{ label, path: \"/changelog\" }` tab. If the project is open source on GitHub and its releases are the product changelog, offer `githubReleases()` (SKILL.md \"Changelogs\").\n\n## URLs and redirects\n\n**The old URL list**: `readme-migration.json` lists every URL the repo served (hidden pages included) with its kind; add `sitemap.xml` (it omits hidden pages and empty parents), `llms.txt`, other versions (`/<version>/docs/…`), and Enterprise or translated segments (`/<project>/`, `/lang-de/`). At the end every entry must be a page or a redirect to one.\n\n- **Status codes.** Use 302 where ReadMe answered with a 302 (empty parents, tag pages, `/`), for link pages, and for section roots (ReadMe's 301 points at whichever page is first, which can change), and the default 301 for moves (operations). The codemod does, except `/`, which it leaves to you (`first_page`).\n- **ReadMe's own redirects** (`redirects` in the settings JSON, or Settings → Errors & Redirects: `old -> new` per line, full-match regex with `$1`, applied only when no page matches): a literal rule → `{ from, to }`; one-segment or rest-of-path patterns → `:name` / `:name*` (`/old(/\\S*)? -> /new$1` → `{ from: \"/old/:path*\", to: \"/new/:path*\" }`, which also sends `/old` to `/new`); anything else → host rules (report).\n- **Versions**: `{ from: \"/<version>/:path*\", to: \"/:path*\" }` (Versions).\n- **The custom 404** (`error404`): rebuild that custom page as `pages/404.astro`, or keep Blume's default. Keep or redirect its `/page/<slug>` URL.\n- **`blume validate`** accepts a redirect `from` as a link target, so rewrite links before trusting it. It warns about a redirect whose `to` leads nowhere (`BLUME_BROKEN_REDIRECT`), but not about loops or chains: run `blume audit --only redirects` after each build. A link to `/llms.txt` or another file Blume generates passes while the config has that feature on.\n- **Check every old URL** against `dist/` and `dist/blume-redirects.json` (no host adapter), following redirects until each lands on a page, or request each from `blume dev` or `blume preview` with `curl -sL`: both answer every redirect, patterns included.\n\n## Teardown\n\n**Disconnect the sync first.** With bi-directional sync on, ReadMe's GitHub app reads and writes every branch named after a version, so restructuring one rewrites the live hub. Migrate on a branch that matches no version (`blume-migration`; `<version>_<name>` would sync), and have the user disconnect the repository (Settings → Git Connection) before merging. Remove every `rdme … upload` step from CI in any shape (and an `rdme` dependency); `rdme openapi validate` can stay as a spec linter.\n\nDelete `custom_blocks/` once converted, `reference/ReadMeConfig/` leftovers, `frontmatter.schema.json`, `hub.html`, `routes.json`, and `readme-migration.json` once the redirects are final (keep `readme-redirects.json`: the config imports it). Scaffold a `package.json` with `blume` and `dev`/`build`/`preview` scripts. Moving the custom domain's CNAME is the user's step: list it.\n\n## Dropped — report these\n\n- **Hosted features**: the editor, Branches and Reviews, the AI Linter and Agent, suggested edits (Blume's edit link comes from `github`), discussions and the dev FAQ, API Metrics, My Developers, My Requests and API logs, the Developer Dashboard, health status updates, and the landing page builder (rebuild as `index.mdx` with `mode: custom`, or `pages/index.astro`).\n- **Personalized docs**: per-reader variables, API keys in Try It, `x-default` credentials, and the `ReadMeConfig` widgets. Blume's Try It takes credentials the reader types.\n- **Login-gated docs** (custom login, JWT, site password, hidden versions): Blume has no sign-in. Point at host-level protection (deployment docs, **Private docs**); it covers the whole site, so public and private pages need two sites.\n- **Changelog options** (layouts, relative dates, author toggle, translations), recipe walkthrough modals, Global Reusable Content across projects, translations through Localize or Transifex (offer `i18n` and `blume translate`), and Enterprise cross-project navigation.\n- **Approximations**: `mode: wide` for `table_of_contents: false`, image sizes and alignment, emoji and unmapped icons, unmapped `x-readme` extensions, unversioned references and changelog, custom components restyled as islands, and HTML or scripts dropped from custom blocks.\n\n<!-- File: .agents/skills/blume-migrate/references/redocly.md (Tokens: ~9842 | Category: anthropic-skill) -->\n# Redocly → Blume\n\nCovers **Redocly Realm** and the products on the same project format (Redoc, Revel, Reef, alone or combined), usually hosted and edited in **Reunite**. A project is a `redocly.yaml`, `.md` pages written in **Markdoc** (`{% tag %}`, not MDX), optional `sidebars.yaml` files, and API description files in the content tree, each rendered as a reference **at its own path**. The work: Markdoc tags become directives and components, each page's body H1 becomes `title`, sidebars become folders and tabs, and every API operation URL changes shape, so the old ones get generated redirects.\n\n## Detect\n\nA **Realm-family project** has at least one of these (`redocly.yaml` is optional; Realm builds a bare folder of Markdown):\n\n- a `sidebars.yaml`, or a prefixed `<name>.sidebars.yaml`, at the root or in a section folder;\n- `.md` pages using Markdoc tags (`{% admonition`, `{% tabs`, `{% partial`);\n- `@theme/`, `_partials/`, `@l10n/`, or `@<version>/` folders, or `*.page.tsx` React pages;\n- a dependency on `@redocly/realm`, `@redocly/redoc`, `@redocly/revel`, `@redocly/reef`, or a combined package (`@redocly/redoc-revel`, `@redocly/redoc-reef`, `@redocly/revel-reef`). Reunite builds projects with no `package.json`, so a missing one proves nothing.\n\nThree look-alikes:\n\n- **A lint/bundle-only `redocly.yaml`**, with none of the above and only keys Redocly CLI reads (`extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `apis`, `resolve`, `telemetry`), configures `redocly lint`/`bundle`. Leave it untouched and migrate the framework the docs actually use.\n- **The legacy Redocly Developer Portal** (Gatsby; `siteConfig.yaml`, `theme.ts`, a `@redocly/developer-portal` dependency, `.mdx` pages, `*.page.yaml` reference pages, and a `sidebars.yaml` that maps named sidebars to groups of `pages:`). Its content is MDX with `:::` admonitions, so this file's Markdoc mappings don't apply: use SKILL.md's general model, this file's Config, API reference, and URL sections, and tell the user it was the legacy portal.\n- **A standalone Redoc embed** (an HTML page loading `redoc.standalone.js` with `<redoc spec-url>` or `Redoc.init`, or a `redocly build-docs` script): migrate only the spec, with `openapi({ spec })`, and rewrite links to its `#tag/…`/`#operation/…` anchors. A Redoc-product project (`@redocly/redoc` alone) has no Markdown either.\n\n**Keep the content where it is**: `content.root: \".\"`, with `content.include` scoped to the real content folders (`[\"guides/**/*.{md,mdx}\", \"apis/**/*.{md,mdx}\"]`, per `references/monorepo.md` §1), plus each root-level page by name (`\"index.{md,mdx}\"`, `\"changelog.{md,mdx}\"`). A root `*.md` glob would also publish `README.md`, which Realm never did. Routes then keep their paths, `/`-rooted partial paths keep resolving, and the description files stay where `redocly.yaml`'s `apis.*.root` and the lint scripts expect them.\n\n**Realm publishes every `.md` in the project** except the root `README.md` and `public/`, partials folders, `static/`, `@theme/`, and the `ignore` globs, so the old sitemap often lists accidental pages: `.github/pull_request_template`, files under `.claude/`, planning notes. Leave those out of `content.include`, add no redirect for them, and list them in the report so the user can add one if they want full URL coverage. Underscore-prefixed files outside a partials folder were pages in Realm but are excluded by Blume's default `**/_*`: rename any that are real pages (`content.exclude: [\"!**/_*\"]` would publish every partial too).\n\n## Config: `redocly.yaml` → `blume.config.ts`\n\nResolve `$ref`s first (`redirects: { $ref: ./redirects.yaml }` is common). Product folders can hold their own `redocly.yaml`, and `env.<environment>` blocks override per environment: map the production values and report the rest.\n\n| Redocly | Blume | Notes |\n| --- | --- | --- |\n| `seo.title` (`seo.projectTitle` when set) | `title` | Blume appends ` - <title>` to every docs page's `<title>` (not to a page titled the same as the site). Realm added a suffix only with `projectTitle`, so on most Realm sites this is a visible change: report it |\n| `seo.description` | `description` |  |\n| `seo.siteUrl` | `deployment.site` | unless the new host is Vercel or Netlify (auto-detected). Cloudflare Pages exposes only a per-deploy URL, so keep it there |\n| `seo.meta` (`[{ name, content }]`), `seo.keywords` | `seo.metatags` (`{ [name]: content }`, `keywords: \"a, b\"`) | minus the tags Blume writes itself; the build names the setting that owns a refused tag |\n| `seo.lang` | nothing for English; else `i18n: { defaultLocale: \"<code>\", locales: [{ code: \"<code>\", label: \"<language name>\" }] }` | `label` is required |\n| `seo.image`, `seo.jsonLd`, `seo.llmstxt` | **drop** (report) | Blume generates per-page cards, JSON-LD, and `llms.txt`; a page in `llmstxt.excludeFiles` gets `ai.exclude: true` |\n| `logo.image` / `logo.srcSet` (`\"./l.svg light, ./d.svg dark\"`), `altText`, `link` | `logo` / `logo: { image: { light, dark, alt }, href }` | move the files into `public/`; a wordmark also gets `text: \"\"` |\n| `logo.favicon` | **drop the field**: copy the file to `favicon.<ext>` or `icon.<ext>` in the root or `public/` | detected by filename, for `.svg`, `.png`, and `.ico` only: convert any other format |\n| `navbar.items` | `navigation.tabs` / `actions` / `cta` | see Navigation |\n| `footer.items` (columns, each a `{ group, items }` or a single item) | `footer.links` (one row) + `footer.socials` | flatten every column's items in order (`page` → its route). Links to a platform in Blume's `footer.socials` list (`docs/configuration/index.mdx`) become socials; a podcast (Spotify, Apple Podcasts) is `podcast`; others (TikTok) stay links. `copyrightText` → `footer.copyright`. Column labels drop (report) |\n| `redirects` (`'/from': { to, type }`) | `redirects: [{ from, to, status: type }]` | write `from` without a trailing slash; a trailing `/*` carries over as written (`{ from: \"/a/*\", to: \"/b/*\" }`), and also matches the bare `/a`. Blume takes 301/302/307/308 only |\n| `markdown.toc` | `toc: { maxHeadingLevel: <depth> }`, or `toc: false` for `hide` | Realm's default depth 3 is Blume's default |\n| `markdown.lastUpdatedBlock` | `lastModified: \"git\"`, **only when declared** and not `hide: true` | Realm shows \"Last updated\" by default; if the source left it at that, offer `lastModified: \"git\"` in the report. Either way, every page the migration edits (nearly all of them, since each H1 moves) shows the migration commit's date; a rename alone keeps the page's date. Say so |\n| `markdown.editPage.baseUrl` | `github: { owner, repo, branch, dir? }` | the path after the branch → `dir`; a non-`github.com` origin → `host` |\n| `ignore` | `content.exclude: [ … ]` | the globs add to Blume's defaults (`**/_*`, `**/.*`), so `_partials/` stay out of routing |\n| `colorMode` | `theme.mode` | `ignoreDetection` with a first mode of `dark` → `\"dark\"` |\n| `banner` (a list) | `banner: { content, link: { href, text }, dismissible }` | one banner: keep the first unscheduled, untargeted one. `content` is plain text, so a Markdown link in it becomes `link`; colors, targets, schedules drop |\n| `feedback` | `hide: true` → `feedback: false`; `type: comment` → `{ comments: true }` | other types drop |\n| `search` | omit (built-in Orama); `suggestedPages` → `search: { provider: orama(), popular }` (`orama` from `blume/search`) | `engine` and `filters` (facets) drop |\n| `analytics` | `ga.trackingId` (a `G-` id) → `googleAnalytics({ id })`, `gtm.trackingId` → `googleTagManager({ id })`, `amplitude.apiKey` → `amplitude({ key })`, `segment.writeKey` → `segment({ key })`, `heap.appId` → `heap({ id })`, `adobe.scriptUrl` → `adobe({ url })`, `fullstory`/`rudderstack` → `script()` | from `blume/analytics`. A `UA-` id is Universal Analytics, dead since 2023: drop it and say so |\n| `scripts.head` / `scripts.body` | one `script({ src, strategy, attributes })` each in `analytics` | local `./static/x.js` → `/x.js`; `async`/`defer` → `strategy`. **Adapters in `analytics` load only in production builds and wait for consent when `consent` is set.** A script the page needs to work (a support widget, a form embed that buttons call) goes in a `PageFooter` layout slot component instead (`<script is:inline src=\"…\">`), which runs in `blume dev` too. That slot renders on every content page, API operation pages included, but not on `mode: custom` or `frame` pages or custom `pages/`: report where the script is now missing |\n| `links` | a Google Fonts stylesheet → `theme.fonts` (`{ body, display, mono }`) | each role a curated slug, or `{ name: \"Family\" }` for anything else; mind renamed families (Source Sans Pro is `source-sans-3`). `preconnect` links drop |\n| `access` (`requiresLogin`, `rbac`, `sso`, `idps`), root `rbac`/`requiresLogin`/`sso`, `ssoDirect`, `userMenu` | **drop** | Blume has no login: point at host-level protection (deployment docs, **Private docs**), which covers the whole site; report every protected path |\n| `l10n` (older: `i18n`) | `i18n` | see i18n |\n| `apis`, `openapi`, `graphql` | `reference` adapters; **keep `apis` in `redocly.yaml`** | see API reference |\n| `products` (`{ id: { name, folder, icon } }`) | `navigation.selectors: [{ kind: \"product\", label, items: [{ label: name, path: \"/<folder>\" }] }]` |  |\n| `aiAssistant`, `mcp` | nothing | offer `ai.assistant` / `agents.mcp` (server output) as follow-ups |\n| `mockServer`, `catalogClassic`, `entitiesCatalog`, `scorecard*`, `developerOnboarding`, `apiFunctions`, `skills`, `corsProxy`, `responseHeaders`, `reunite`, `removeAttribution`, `palette`, `breadcrumbs`, `codeSnippet`, `navigation`, `navbar.hide`, `footer.hide`, `sidebar.hide` | **drop** (report) | `consent` → a `blume/consent` adapter if the site runs one; `navigation.actions` are Blume's default page actions |\n| `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, `recheck` | **keep in `redocly.yaml`** | see Teardown |\n| `'{{ process.env.X }}'` | `process.env.X` in `blume.config.ts` |  |\n\n## Navigation: `sidebars.yaml` and `navbar` → folders and tabs\n\nWith no sidebar file, Realm generates the sidebar from the file tree (natural sort, `index` first), which Blume's generated sidebar reproduces. Otherwise every file ending in `sidebars.yaml` is a sidebar (one only pulled in through another's `$ref` isn't), `page:` paths resolve from the sidebar file (or the project root with a leading `/`), and **a page no sidebar lists shows no sidebar** (unless its frontmatter `sidebar.path` names one): in Blume's generated tree it would appear. `sidebar.hidden: true` hides it, but also drops it from search, the sitemap, and `llms.txt`, where Realm still listed it: hide only orphans nobody should find, and report which you hid. Realm labels default to the page's `seo.title`, then its first heading, so most `label`s only restate the new `title`.\n\n- **`group` + `items`** → a folder with a `meta.ts` (`title`, and `pages` in the declared order). A group whose pages sit flat beside others, or come from several folders, has no on-disk counterpart: move its pages into a subfolder (a page the group opened becomes its `index`, keeping its URL; the others get redirects) or use an explicit `navigation.sidebar`, per SKILL.md \"Config-declared nesting\".\n- **`label`** → `sidebar.label`, only where it differs from `title`. **`href`** → an explicit sidebar link or `navigation.featured`. **`directory`** → leave the folder to the generated sidebar. **`$ref`** → inline it.\n- **`expanded`**: Realm groups collapse by default, which `navigation.sidebar.display: \"group\"` reproduces. `expanded: true`/`always` → `collapsed: false` in that folder's `meta.ts`. **`menuStyle: drilldown`** → `display: \"page\"`.\n- **`separator`** and **`separatorLine`** have no equivalent: a labeled separator heading a run of pages is usually better as a group; report the rest.\n- **`badges`** → `sidebar.badge` (first name). **`icon`** → Lucide (Icons). `rbac`, `disconnect`, `additionalProps`, `external`, `*TranslationKey` drop.\n- **An API description listed as a `page`** places that API's generated tree. Blume can't put generated operation pages into an explicit `navigation.sidebar`; they sit wherever the source's `route` puts them (see API reference).\n\n**Several sidebars → header tabs.** Realm gives each page the sidebar that lists it, so one sidebar file per section (`guides/sidebars.yaml`), or top-level `tab:` entries in `sidebars.yaml`, is a tabbed site: one `navigation.tabs` entry per section folder (a `tab` with `menu:` → a tab with `items`). The root sidebar's pages, if they share no folder, get a tab with `path: \"/\"` and an `href` to their landing page; Blume picks the longest matching tab path, so section routes still select their own tab.\n\n**`navbar.items`**: an item that opens a section with its own sidebar is that section's tab. The first item into the root sidebar is the root tab; other items into the same sidebar become `navigation.actions`, or drop if they duplicate a tab's target. A \"Sign up\" button → `navigation.cta` with an absolute URL; a GitHub link → `github`.\n\n## Content: Markdoc → MDX\n\nBlume parses `.md` as plain Markdown; directives, components, and ` ```mermaid ` fences need `.mdx`. **Rename only the pages that end up with one of those** (`<include>` works in `.md` too), and leave the rest `.md`, so the diff stays small and their history keeps its file names. Then fix what MDX parses differently, in pages and in every partial an `.mdx` page includes (an included partial parses as MDX):\n\n- **Angle-bracket autolinks** (`<https://…>`, `<team@acme.com>`) fail to compile: write `[text](url)` or a bare URL.\n- A literal `{` or `}` in prose: escape it or put it in inline code. **HTML comments** → `{/* */}`.\n- **Raw HTML** must be JSX-valid: string `style=\"…\"` → `style={{ … }}` or a class in `theme.css`; close void tags (`<br />`). `<figure>` + image + `<figcaption>` reads better as `<Frame caption=\"…\">` around the image. A raw `<iframe>` of a YouTube video → `<YouTube id=\"…\" />`.\n- **File links follow the rename**: Blume resolves `./setup.md` by source file, trying the other extension when that file is gone, so a link to a page renamed `setup.mdx` still lands on it, `slug` and numeric prefix included. Update them anyway: a link to the old name breaks wherever the Markdown is read as files, such as on GitHub.\n\n**Every Markdoc tag must be converted**: in `.md` an unconverted tag ships as literal text with a green build, and `blume check` and `build` only warn `BLUME_TEMPLATE_TAG` at its line (in `.mdx` the page fails to compile, and `blume check` and `validate` report it as `BLUME_MDX_SYNTAX`). Finish with `grep -rn '{%' <content folders>` returning only samples that document Markdoc itself, in code where the warning skips them.\n\n**The body H1 is the title.** Realm renders the page's first `# H1` and uses it for `<title>`, sidebar label, and breadcrumb (unless the page sets `seo.title`; a page with no H1 uses its first heading of any level). Move it to frontmatter `title`, delete it from the body, and demote any later H1 to `##`. Links to the H1's own anchor (`#<h1-slug>`, often the first entry of a hand-written contents list) now break: remove them or point them at the page.\n\n| Markdoc | Blume (`.mdx`) |\n| --- | --- |\n| `{% admonition type=\"info\" name=\"Title\" %}` … `{% /admonition %}` | `:::info[Title]` … `:::` (`success`, `warning`, `danger` keep their names; `idea` → `tip`). No `name` → no brackets |\n| `{% tabs %}` / `{% tab label=\"X\" %}` | `<Tabs>` / `<Tab title=\"X\">`; `tabs id=\"k\"` → `param=\"k\"`. **A group of tabs that each hold only a code fence** → `<CodeGroup>` with titled fences (` ```ts title=\"X\" `); a bare fence directly inside `tabs` was a tab named by its `label` |\n| `{% code-group %}` | `<CodeGroup>` with titled fences; `mode=\"dropdown\"` → `<CodeGroup dropdown>` |\n| `{% partial file=\"…\" variables={a: \"x\"} /%}` | `<include a=\"x\">…</include>` on its own line (Partials) |\n| `{% raw-partial file=\"…\" /%}` | inline the file's text |\n| `{% code-snippet file=\"./x.js\" title=\"x.js\" /%}` | `<include lang=\"js\" meta='title=\"x.js\"'>./x.js</include>` (the file inside `content.root`). `from`/`to`/`before`/`after` ranges: paste the lines into a fence instead (report) |\n| `{% cards columns=N %}` / `{% card title to icon image %}` | `<CardGroup cols={N}>` / `<Card title href icon img>`; `layout=\"horizontal\"` → `horizontal` |\n| `{% accordion-group %}` / `{% accordion title expanded %}` | `<Accordion>` / `<AccordionItem title defaultOpen>` |\n| `{% numbered-list %}` / `{% numbered-item %}` | `<Steps>` / `<Step title>` (an item's leading heading becomes `title`) |\n| `{% img src alt caption /%}` | `![alt](src)`, inside `<Frame caption=\"…\">` when it had a caption. `srcSet` light/dark pairs have no equivalent: keep one image (report) |\n| ` ```mermaid ` fence, `{% diagram type=\"mermaid\" %}` | a ` ```mermaid ` fence (inline the file), in an `.mdx` page. PlantUML and Excalidraw have no renderer: export to SVG (report) |\n| `{% icon name=\"…\" /%}`, `{% inline-svg file /%}` | `<Icon icon=\"<lucide-name>\" />`, `![alt](./file.svg)` |\n| `{% table %}` | a pipe table (raw HTML for `colspan`/`rowspan`) |\n| `{% if %}` / `{% else /%}` | no conditionals: keep the branch the live site showed; per-reader branches (`$rbac`, `$user`) keep the public one (report) |\n| `{% $frontmatter.x %}` / `{% $env.PUBLIC_X %}` | the literal value / `{{public-x}}` with a `variables` entry (`\"public-x\": process.env.PUBLIC_X ?? \"\"`: values must be strings, so an unset variable would fail the config) |\n| `{% json-schema %}`, `{% json-example %}`, `{% openapi-code-sample %}`, `{% openapi-response-sample %}`, `{% replay-openapi %}` | a fenced sample or a `TypeTable`, or a link to the operation page, which renders samples and Try it itself (report) |\n| `{% markdoc-example %}`, `{% login-button %}`, `{% connect-mcp %}`, `{% code-walkthrough %}` | the fence only / drop (report); a walkthrough becomes prose and fences or `<Steps>` |\n| `## Heading {% #id %}` / `{% .class %}` | `## Heading [#id]` / drop |\n| any other tag | a custom tag from `@theme/markdoc/schema.ts`: rewrite its markup, or make its component an island (`islands/<Name>.tsx`, used as `<Name …/>`) |\n\n**Code fences**: Realm reads fence meta only from an annotation: ` ```js {% title=\"app.js\" highlight=\"{2,4-6}\" %} ` → ` ```js title=\"app.js\" {2,4-6} `, and `highlight=\"/word/\"` → `// [!code word:word]`. A bare word after the language was ignored by Realm but becomes a title in Blume: delete stray ones. `[!code highlight|++|--|word|focus|error|warning]` comments carry over.\n\n**Partials** live in `_partials/` folders at any depth, which Blume also keeps out of routing. `markdown.partialsFolders` replaces that default: add the folders it names to `content.exclude` (which keeps Blume's defaults) or rename them with a leading `_`. A leading `/` in `file` meant the project root, which is `content.root` when that's `\".\"`. Variables become lowercase attributes (`favFood` → `favfood`), read in the partial as `{{favfood}}`; a partial that read `$frontmatter` needs the value passed in. A partial used only by `.md` pages can stay `.md`.\n\n## Frontmatter\n\n| Redocly | Blume |\n| --- | --- |\n| body `# H1` | `title` |\n| `title` (top level) | **drop.** Realm never rendered it on Markdown pages (`<title>` was the H1), so moving it to `seo.title` would change live titles. Offer it in the report when it looks like a deliberate SEO title |\n| `description` (top level) | `description` (Realm used the site description instead; this improves the meta description) |\n| `seo.title`, `seo.description`, `seo.image` | the same keys (`seo.title` was also Realm's sidebar label: copy it to `sidebar.label` to keep that) |\n| `seo.keywords` | `search.keywords`, as a list (a string splits on commas). Blume uses them as search terms and writes no per-page `<meta name=\"keywords\">` |\n| `excludeFromSearch: true` | `search.exclude: true` + `ai.exclude: true` (+ `noindex: true` to keep it out of the sitemap, as Realm did) |\n| `slug` | `slug` (a full route); a list → the first as `slug`, the others as redirects |\n| `redirects: { '/old/': {} }` | a config `redirects` entry per path, `from` without the trailing slash. Exact entries beat wildcards, as in Realm |\n| `sidebar: { hide: true }` (hides the sidebar on this page; it doesn't take the page out of one) | `mode: center`. Not `sidebar.hidden`, which removes the page from the navigation, search, and the sitemap |\n| `sidebar: { path }` (which sidebar this page shows) | drop: the tab whose `path` holds the page decides its sidebar |\n| `navigation.nextButton` + `previousButton` `hide: true` | `pagination: false` |\n| `navbar`/`footer` hidden | `mode: custom` (the header only: no sidebar, title, or footer). The header always shows: report a hidden navbar |\n| `rbac` | drop; report the page as access-controlled |\n| `markdown.*`, `codeSnippet`, `breadcrumbs`, `colorMode`, `feedback`, `search`, `versionPicker`, `banner`, `template`, `keywords` (Typesense search curation), `seo.meta`/`jsonLd`/`lang`/`priority` | drop (report what was set) |\n| top-level `editPage` (including legacy `editPage: { disable }`) | drop without reporting: Realm reads only `markdown.editPage`, so it never had an effect |\n| custom keys (`metadata`, …) | `frontmatter.extend` if the site needs them, else drop |\n\n## API reference\n\nRealm renders every description in the content tree (a `.yaml`/`.json` with a top-level `openapi:`, `swagger:`, or `asyncapi:` key, or a `.graphql` schema) **at its path without the extension**: an overview at `/apis/orders/openapi`, a page per tag, and a page per operation below it. `apis` entries match files by `root` and carry lint and display settings; a description needs no entry to render. List every description, and report any `apis.*.root` that points at a missing file.\n\n- **Use `openapi()`** from `blume/reference`, one source per description: like Realm, it gives each operation its own page and URL. (`scalar()` would collapse each API onto one page.) AsyncAPI → `asyncapi()`; GraphQL → `graphql()` with `endpoint`. Leave the files where they are.\n- **Choose each `route`.** When the description shares its folder with guides (`apis/orders/openapi.yaml` beside `apis/orders/quick-start.md`), keep the old path (`route: \"/apis/orders/openapi\"`): its overview URL survives, and the reference sits in that folder's group beside the guides. **When the description is alone in its folder** (`apis/orders/openapi.yaml` and nothing else), the old path nests the reference's group inside a folder group that holds only it: use the folder as the route (`route: \"/apis/orders\"`), so the reference is that group. That moves one overview URL, which the catch-all below covers. Either way, `label` (the `sidebars.yaml` label, else the spec's `info.title`) names the group. Choosing a route inside a topic folder elsewhere moves the reference to where a `sidebars.yaml` had placed it, at the cost of more redirects; say which you chose. Add a `navigation.tabs` entry if an API was a top-level section.\n- **Spec problems fail `blume validate --strict`** (`BLUME_OPENAPI_*` warnings, e.g. path parameters declared `in: query` or missing). Fix them with an OpenAPI Overlay in the source's `overlays`, leaving a generated spec untouched, and report them for a fix at the source.\n- **Decorators don't run.** Realm applies `decorators` (`remove-x-internal`, `filter-out`, `info-override`), and the `preprocessors` and `plugins` behind them, when it bundles a description; Blume reads the raw file. When any are configured, point `spec` at `redocly bundle <api> --output <file>` output or express the change as an Overlay.\n- **Tag labels and order.** Blume reads `x-displayName` for each tag group's label, and orders tags as the spec's `tags` list does and operations as the spec lists them. `x-tagGroups` isn't read: when the spec has them and their flattened order (how Realm ordered tags) differs from `tags`, write `<route>/<tag-slug>/meta.ts` with `defineMeta({ order })`, the tag's position there (the folder under `content.root`, mirroring the route; it must sit in a folder `content.include` reaches, or `BLUME_META_OUTSIDE_INCLUDE` warns that it's never read). It merges over the generated title and order. Get each tag's folder name from Blume's routes (below), not by slugifying. `x-tagGroups` names drop, and tags left out of every group, which Realm hid, now appear: report both. `x-traitTag`, `x-logo`, and `x-badges` are ignored.\n- **Code samples**: `codeSamples.languages[].lang` (root and per-API `openapi:`) → `codeSamples`; Blume reads Redocly's names (`curl`, `JavaScript`, `Node.js`, `Python`, `Java`, `C#`, `PHP`, `Go`, `Ruby`). `R`, `Payload`, `Java8+Apache`, and `C#+Newtonsoft` have no generator and warn `BLUME_OPENAPI_UNKNOWN_CODE_SAMPLE`: drop them and report. With no languages set, Realm showed its full default list and Blume shows `curl`, `js`, `python`: report the difference. `x-codeSamples` render natively. `codeSamples`, `playground`, and `expandSchemas` are `openapi()` options, shared by all its sources: APIs whose settings differ go in separate `openapi()` entries.\n- **Try it**: `hideReplay: true` → `playground: false`. Realm proxied Try it requests by default; Blume sends them from the browser. Set `playground: { proxy: \"<url>\" }` only for a proxy the user runs; **never carry over a Redocly-hosted one** (`/_api/cors/`, the default; `https://cors.redocly.com`; `https://cors.redoc.ly`). Probe each API's server for CORS with `curl -si -X OPTIONS -H 'Origin: https://<docs-host>' -H 'Access-Control-Request-Method: GET' <server-url>/<some-path>` and list the ones without a matching `access-control-allow-origin` as a follow-up: `playground: { proxy: true }` fixes them once the user picks a server-output host.\n- **Other display options**: `excludeFromSearch` → the source's `includeInSearch: false`; `schemasExpansionLevel: all` → `expandSchemas: true`; `layout`, download buttons, `showExtensions`, `feedback` (Blume's page rating shows on every overview and operation page, with no per-source switch), `mockServer`, and the rest drop (report).\n\n## URLs and redirects\n\n**Save the old URL list first**: a Realm site with `seo.siteUrl` serves `<site>/sitemap.xml`, listing every page **and every API overview, tag, `section/`, and operation page**. Every URL on it must end up a Blume page or a redirect to one.\n\n- **Page routes.** Realm slugs the file path: lowercased, spaces → `-`, `_` and `.` kept, **numeric prefixes kept**. Blume keeps the filename's case and its spaces, and strips a numeric prefix followed by `-`, `_`, or `.` (`01_setup` → `/setup`; a date like `2024-01` or a version stays whole). Rename mixed-case and spaced files to match; a numbered file needs `slug` (or a redirect) to keep its number; a nested `README.md` (Realm `/…/readme`, Blume `/…/README`) becomes `readme.md` to keep its URL.\n- **Moved pages** (materialized groups, `index` promotions, tabs) and **Realm's own redirects** (config, frontmatter `redirects`, extra `slug`s) all become `redirects`. A long generated list reads best in its own `redirects.ts` module, imported into `blume.config.ts`.\n- **Operation pages change shape.** Realm: `<old route>/<tag-slug>/<operationId lowercased>` (`getMultipleDomains` → `getmultipledomains`, `Domains.getDomain` → `domains.getdomain`), or `<old route>/<tag-slug>/paths/<JSON pointer>/<method>` with no `operationId` (`…/paths/~1pets~1%7Bid%7D/get`). Realm tag slugs keep underscores. Blume's slugs differ, so write **one exact redirect per operation**, generated:\n  1. `blume build` once with the references configured.\n  2. `node <skill>/scripts/operation-routes.mjs dist > operation-routes.json` prints, per reference route, every endpoint (`GET /pets/{id}`, or `POST petAdopted` for a webhook) mapped to its Blume route, read from the build itself (a Vercel server build's pages are in `.vercel/output/static`: pass that). `--spec <route>=<file.json>` adds operationIds (convert YAML first: `npx @redocly/cli bundle openapi.yaml --output openapi.json`).\n  3. For each old operation URL, find its operation in the spec: match the last segment against each `operationId` lowercased, or decode the pointer (`%7B` → `{`, `~1` → `/`, `~0` → `~`) to a path plus method. Look up `METHOD path` under the new route, and emit `{ from: <old URL path>, to: <route> }`.\n- **Tag, `section/`, and tag-description heading pages** (`<old route>/<tag>`, `<old route>/section/<heading>`, `<old route>/<tag>/<heading>`) don't exist in Blume: send them to the overview. When the route moved, one catch-all does it: `{ from: \"<old route>/:path*\", to: \"<new route>\" }` (exact operation entries still win). When the route stayed, use `{ from: \"<route>/section/:path*\", to: \"<route>\" }` and `{ from: \"<route>/:tag\", to: \"<route>\" }`, plus exact entries for tag headings; a pattern that covers a page fails the build (`BLUME_REDIRECT_MATCHES_PAGE`).\n- **Legacy hash links** (`#operation/<id>`, `#tag/<tag>`, `#section/…`) worked on Realm, which resolved them in the browser, and still appear **in content, partials, and the description files' own descriptions**. A server can't redirect a fragment. `blume validate` flags one on a page that still exists as a broken anchor, but accepts any redirect `from` as a link target, so it misses the ones whose path now redirects: `grep -rn '#operation/\\|#tag/' <content> <spec files>` and rewrite each to its Blume route. Rewrite absolute links to the site itself (`https://<docs-host>/…`) to root-relative paths too; `blume audit` flags the ones left.\n- **Trailing slashes.** Realm answered `/a/` with a 301 to `/a`, and so do `blume dev` and `blume preview`, for pages and redirect sources alike. In production it depends on the host: static hosts serve a page's `a/index.html` at `/a/`, and server builds (`vercel()`, `node()`) match `/a/` for a redirect `from`, but whether a static host applies a redirect rule to the slashed form varies. When old links used trailing slashes (frontmatter `redirects` often did), check one slashed redirect on a preview deployment and report it.\n- **Versions and locales**: Realm drops the `@` from version folders (`api/@v1/x.md` → `/api/v1/x`) and serves the default version without its segment; non-default locales get the lowercased code (`@l10n/es-ES/` → `/es-es/…`).\n- **Check coverage**: after the build, every old URL should be a page in `dist/` (`<path>/index.html`) or resolve through `dist/blume-redirects.json` (exact entries first, then patterns) to one. That manifest is written for a static build with no host adapter; with one, request each URL from a running `blume dev` (or `blume preview` of a static build) instead: both answer every redirect, patterns included. `blume audit --only redirects` checks the redirects themselves.\n\n## Assets, theme, React pages, and the repo\n\n- **`static/` → `public/`**: Realm serves `static/` at the root, so root-relative references keep working. Drop a Redocly-hosting `robots.txt` (`/~/`, `/cdn-cgi/` rules); Blume writes its own.\n- **`.gitignore`**: Realm projects often list `public/` (and sometimes `_partials`), which would leave moved files uncommitted: remove those lines. Add `.blume/`, `dist/`, and `.env.local`, as `blume init` does.\n- **Node**: Blume needs Node 22.19 or later, and Realm templates often pin 20 in `.nvmrc`. Update `.nvmrc`/`.node-version`, `engines.node`, and CI `node-version`.\n- **`@theme/styles.css`**: the brand color (`--color-primary-base` or the variable it points at) → `theme.accent`; check it against dark mode, which `blume audit` checks for contrast. Rules for the site's own classes (a landing page, figures) move to a root `theme.css`, with `html.dark` selectors rewritten as `[data-theme=\"dark\"]`. Rules for Realm's components don't apply: report them.\n- **`@theme/components/*`**: a component a page uses → an island (`islands/`, or imported by a custom page with `client:*`); an ejected Realm component → the matching layout slot if one exists, else drop. `@theme/layouts`, `Templates`, `plugin.*`, and `markdoc/` drop once their tags are converted.\n- **`*.page.tsx`**: `index.page.tsx` → `pages/index.astro` on `PageLayout`, with interactive parts as React islands, or an `index.mdx` with `mode: custom` when it's mostly links. Its `export const frontmatter` gives the title. `404.page.tsx` → `pages/404.astro`, or Blume's built-in 404. A React-state light/dark image swap becomes two images with CSS hiding one.\n\n## i18n\n\n`l10n: { defaultLocale, locales: [{ code, name }] }` → `i18n: { defaultLocale, locales: [{ code, label: name }] }`. Pages under `@l10n/<code>/` move to `<code>/` at the content root. Blume uses `code` as written in URLs, so write it lowercase (`es-es`, folder too) to keep Realm's URLs. `translations.yaml` label keys → per-locale `meta.ts` titles and tab label maps; UI strings → `i18n.ui` where Blume has one; report the rest.\n\n## Versions\n\nRealm versions a folder through `@<version>/` subfolders and an optional `versions.yaml` (`default`, order; with no `default`, the last listed version is the default); the default version serves without its segment. Blume's versioning is site-wide:\n\n- **Version folders at the content root** → Blume versioning: the default version becomes the current tree (`versions.current: { label }`, required), the others top-level folders listed newest first in `versions.archived` as `{ id, label? }` (ids start with a letter: `@1.0` → `v1.0`, redirect `/1.0/:path*`).\n- **A versioned section** (`api/@v1/`) → plain folders reproduce the URLs: the default version's files move up into `api/`, the others into `api/v1/` (a name like `1.0` stays whole). Add a `navigation.selectors` entry `{ kind: \"version\", … }` and report the missing old-version notice and version-scoped search. Versioned descriptions → one `openapi()` source each.\n\n## Icons\n\nRealm icons are Font Awesome names (`book`, `solid check-circle`, `brands github`) or image paths. Strip the pack prefix and map to Lucide with the table in `references/mintlify.md`; image paths work as written; brand icons other than the few Lucide ships (listed there, `github` among them) have no Lucide match (report).\n\n## Teardown — keep lint and bundle working\n\n1. **Trim `redocly.yaml`, don't delete it**: remove the portal keys and keep `extends`, `rules`, `decorators`, `preprocessors`, `plugins`, `resolve`, `telemetry`, and each `apis` entry's `root`, `output`, `overlays`, and lint keys (drop its `openapi`/`graphql`/`theme` display keys). If nothing but `apis` roots remain, keep it anyway when scripts or skills run `redocly lint`; otherwise ask. Run the lint script before and after: the result should match.\n2. Keep `@redocly/cli` and its scripts and CI jobs. Remove the Realm-family packages, `styled-components` and the other template peers nothing else imports (keep `react`/`react-dom` if an island uses them), and the `realm` scripts; add `blume` and the `blume dev`/`build`/`preview` scripts.\n3. Delete `sidebars.yaml` files, `versions.yaml`, `@theme/`, the React pages once rebuilt, `translations.yaml` once mapped, the `tsconfig.json`/`types.d.ts` that only served them, and the redirects file a `$ref` pointed at.\n4. Reunite settings (custom domain, environment variables, teams, hosted analytics) live outside the repo: list what the user must recreate.\n\n## Dropped — report these\n\nLogin, RBAC, and SSO (every protected path); Reunite's editor, reviews, previews, and feedback dashboard; the API explorer's mock server, environments, saved auth, and default CORS proxy (and which APIs now fail CORS); `x-tagGroups` names and the tags they hid; API tag and `section/` pages (redirected); sidebar separators and `expanded: always`; footer column labels; the ` - <title>` suffix now on every `<title>`; dropped frontmatter titles; `seo.image`/`jsonLd` and `llms.txt` sections; extra banners; search facets; resolved `if` branches and per-reader content; `code-snippet` ranges; the Markdoc tags with no equivalent; PlantUML/Excalidraw; light/dark image pairs; unmatched icons; ejected components and Realm CSS; a `UA-` analytics id; scripts that now load only in production; trailing-slash URLs; the accidental pages left out; reset \"last updated\" dates.\n\n<!-- File: .agents/skills/blume-migrate/references/starlight.md (Tokens: ~3769 | Category: anthropic-skill) -->\n# Starlight → Blume\n\nStarlight is an Astro integration configured via `starlight({…})` in `astro.config.*`, with content in `src/content/docs`. Both are Astro, so this is a natural fit — content can mostly stay in place, with one big caveat: **most Starlight content is `.md`, and Blume's `:::` directives are MDX-only** (see Asides below).\n\n## Detect\n\n- `astro.config.{mjs,mts,ts,js,cjs}` importing and calling **`starlight({…})`** (`import starlight from \"@astrojs/starlight\"`).\n- Content under **`src/content/docs/`**; a `src/content.config.ts` (current) or `src/content/config.ts` (pre-Astro-5).\n- `@astrojs/starlight` dep.\n\nKeep content where it is — set `content.root: \"src/content/docs\"`.\n\n## Config: `starlight({…})` → `blume.config.ts`\n\n**Harvest the surrounding `astro.config.*` too, not just the `starlight()` call:** top-level Astro `redirects` → Blume `redirects`; `site` → `deployment.site`, unless the target host is Vercel or Netlify, which Blume auto-detects (see SKILL.md); other integrations → report.\n\n| Starlight option | Blume |\n| --- | --- |\n| `title` | `title` (if per-locale/computed, set manually) |\n| `description` | `description` |\n| `logo` (`{ src }` or `{ light, dark, alt }`) | `logo` — move the referenced file into `public/` |\n| `logo.replacesTitle` | `logo: { text: \"\" }` (renders the mark alone) |\n| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |\n| `social` (array of `{ label, icon, href }`; pre-0.33 legacy: `{ github: url }` object) | derive **`github: { owner, repo }`** from the GitHub entry; every other entry whose platform Blume knows → **`footer.socials`** (`{ discord: href, bluesky: href, … }`; the `x.com`/`twitter` icon → `x`; the platform list is under `footer` in `docs/configuration/index.mdx`); the rest (`mastodon`, `gitlab`, `rss`, …) drop (report) |\n| `editLink.baseUrl` (`…/edit/<branch>/<subdir?>`) | `github: { owner, repo, branch }` — **and any repo sub-path after the branch → `github.dir`** (a docs-in-subfolder repo breaks every edit link without it); **an origin other than `https://github.com` → `github.host`** (a GitHub Enterprise repo otherwise links to the public site) |\n| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |\n| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |\n| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |\n| `expressiveCode.themes` | `markdown.code.theme: { light, dark }` — EC assigns by theme _type_, so match each theme by its darkness, not by array position |\n| `expressiveCode.styleOverrides` | drop → restyle via a root `theme.css` file |\n| `head` | per entry: a `meta` tag → **`seo.metatags`** (`{ [attrs.name ?? attrs.property]: attrs.content }` — verification tokens, `theme-color`, and the like), minus the tags Blume writes itself (`description`, `robots`, `og:title`/`og:description`/`og:image`/`og:url`/`og:type`/`og:site_name`, `twitter:*` card tags, `article:*` dates — the build refuses them and names the setting that owns each, e.g. `og:image` → `seo.og`, `twitter:site` → `seo.x.handle`); an analytics `<script>` → one `script()` adapter each in the `analytics` list (from `blume/analytics`): `attrs.src` → `src`, an `async`/`defer` attr → `strategy`, every other `attrs` key → `attributes`, and an inline body (`content`) → `content`; anything else (`link`, `style`, other scripts) → drop and report |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `customCss` | drop → move into a root **`theme.css`** file (auto-picked-up; not a config field) |\n| `components` (overrides) | Blume's layout slots via `defineComponents({ layout: { … } })` in a root `components.ts`: `Header`, `Search`, `Sidebar`, `TableOfContents`, `Pagination` keep their names; `SiteTitle` → `Logo`; **`Footer` → `PageFooter`**, not Blume's `Footer` (that slot is the site footer; Starlight's `Footer` is the page-end last-updated date, pagination, and edit link, which Blume renders itself, so keep only what the override added). Other overrides (`Banner`, `PageTitle`, `Hero`, `EditLink`, `SocialIcons`, …) have no slot — fold their additions into `PageHeader`/`PageFooter`, or `blume eject`; a Starlight name left under `layout` matches no slot and does nothing. Rewrite reads of `Astro.locals.starlightRoute` against the slot's props |\n| `plugins` | **map, don't blanket-drop** — see Plugins below |\n| `pagination: false`, `pagefind`/Pagefind options, `titleDelimiter`, `credits`, `disable404Route`, `routeMiddleware` | drop (report) |\n| `locales` / `defaultLocale` | `i18n` (see below) |\n\n## Navigation: `sidebar`\n\nStarlight's `sidebar` array → prefer letting Blume generate from the filesystem; use `navigation.sidebar` only for shapes files can't express:\n\n- `{ label, items: [{ autogenerate: { directory: \"d\" } }] }` (0.39 and later; before 0.39 the group held it directly, `{ label, autogenerate: { directory: \"d\" } }`) → **structure the folder and rely on filesystem nav** (label → the folder's `meta.ts` `title`; `autogenerate.collapsed` → `collapsed: true` in each subfolder's `meta.ts`). Do **not** map it to a sidebar item with `root:` — in an explicit Blume sidebar that renders a single page link, not the directory's children. Since 0.39 a group's `items` can mix manual entries with an `autogenerate` entry; move those manual pages into the folder (route change → `redirects`) so the folder holds the whole group.\n- string `\"guides/intro\"` → `\"/guides/intro\"`; `{ label?, slug }` → `/<slug>`.\n- `{ label, items: [...] }` → `{ label, items: [...] }` (recursive) — or better, a real folder.\n- `{ label?, link }` → `{ label, href: link }`.\n- `badge` (string or `{ text, variant }`) → `badge` (text only; variant drops). `collapsed` (bool) → `collapsed`. Item `attrs` and `translations` → drop (report).\n\n## Frontmatter\n\n| Starlight | Blume |\n| --- | --- |\n| `title` / `description` / `draft` / `slug` | pass through |\n| `sidebar.{label,order,hidden}` | pass through |\n| `sidebar.badge` | `sidebar.badge` (text only) |\n| `sidebar.attrs` | **remove** (strict schema — build error if left) |\n| `pagefind: false` | `search.exclude: true` |\n| `lastUpdated` (date/string) | `lastModified` |\n| `lastUpdated: false` (boolean) | **remove** (Blume's `lastModified` takes a date; a boolean is a build error) |\n| `prev: false` **and** `next: false` | `pagination: false` (hides both links) |\n| `prev` / `next` otherwise (one side, or a label/link) | **drop** (report — the links can only be hidden together, and their labels come from the linked pages) |\n| `template: splash` | `mode: center` (no sidebar, a wider centered column — the closest layout; report) |\n| `hero` | **no key** — its tagline → a paragraph, its actions → `<Card>`s or links, its image → a Markdown image (report); for a designed landing page, rebuild it as a custom `.astro` page under `content.pages` (`pages/index.astro` on `PageLayout` wins over the content page) |\n| `banner`, `tableOfContents`, `editUrl`, `head` | drop (report) |\n\n## Asides → directives (and the `.md` trap)\n\nStarlight's primary callout syntax is the `:::note`/`:::tip`/`:::caution`/`:::danger` directive **in plain `.md` files**. The directive names map perfectly (Blume aliases `caution`→warning; `error`→danger) and `[Title]` syntax carries over — **but Blume only parses directives in `.mdx`**. In a `.md` file, `:::note` renders as literal text and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. So: **rename every `.md` file that contains asides (or math, or mermaid/package-install fences) to `.mdx`** — for a typical Starlight repo that's most of the content; renaming everything to `.mdx` is usually simpler and safe.\n\n- `<Aside type=\"…\" title=\"…\">` (the component form) → the same directives; bare `<Aside>` → `:::note`.\n- An aside custom icon (`:::tip{icon=\"heart\"}`) → drop the attr (report).\n\n## Components\n\n- **Renames:** `<CardGrid>` → `<CardGroup>`; `<LinkCard>` → `<Card>` (its `description` prop drops — fold into the body); `<TabItem label=\"…\">` → `<Tab title=\"…\">`. `<Tabs>` and `<Card>` stay; **keep** a `<Tabs syncKey=\"…\">` prop as is — Blume's `Tabs` takes `syncKey` with the same scoping (only groups sharing the key switch together). One difference: Blume groups without a key also sync, page-wide, by tab title, where Starlight leaves them independent — add `sync={false}` to a keyless group whose same-titled tabs must stay unlinked.\n- **`<Badge>` needs conversion, not pass-through:** Starlight puts content in a `text` prop and uses variants `note`/`tip`/`caution`/`danger`/`success`/`default` with sizes `small`/`medium`/`large`. Blume's `<Badge>` renders **children** with variants `default`/`accent`/`success`/`warning`/`danger` and sizes `xs`/`sm`/`md`/`lg`. Move `text` into the children; remap variant (`note`→`default`, `tip`→`accent`, `caution`→`warning`, `danger`→`danger`, `success`→`success`) and size (`small`→`sm`, `medium`→`md`, `large`→`lg`).\n- **Convert yourself:** `<Steps>` → Blume `<Steps>`/`<Step>`; `<FileTree>` → Blume `<FileTree>`; `<Code code={…}>` → a fenced code block; `<LinkButton>` → a Markdown link or `<Card>`.\n- Strip `import … from \"@astrojs/starlight/*\"` and `astro:assets` lines.\n\n## Code blocks: Expressive Code meta → Blume\n\nStarlight content is full of Expressive Code fence meta; Blume understands some of it and **promotes unknown bare tokens into the code-block title**, so unconverted meta produces garbage headers. Convert per fence:\n\n- `title=\"file.js\"` → works as-is (or use the space-title shorthand). Line ranges `{2-3}` → work as-is.\n- `ins=`/`del=` line marks → `// [!code ++]` / `// [!code --]` comments; `mark=` → `{ranges}` or `// [!code highlight]`.\n- `showLineNumbers` → `lineNumbers` (`BLUME_CODE_FENCE_OPTION` flags any left).\n- `wrap` / `wrap=true` → a bare `wrap` (that block's long lines wrap); a site-wide `expressiveCode.defaultProps.wrap: true` → `markdown.code.wrap: true`.\n- **Drop:** `frame=\"terminal\"`, `collapse=`, `wrap=false`, `preserveIndent`/`hangingIndent`, `\"string\"` and `/regex/` text markers (report if they carried meaning).\n- ` ```diff lang=\"js\" ` → a normal ` ```js ` fence with `[!code ++]`/`[!code --]` markers.\n\n## Plugins — map, don't drop\n\n- `starlight-openapi` → an `openapi({ sources })` entry in Blume's `reference` list, imported from `blume/reference` (delete any generated pages; add the `navigation.tabs` entry).\n- `starlight-blog` → `type: blog` pages (RSS at `/blog/rss.xml`). Blume generates **no** blog index, tag, or author pages: write a `blog/index.mdx` whose `CardGroup` links each post (see `docs/advanced/blog.mdx` in the installed package), and report the tag and author pages as dropped.\n- `starlight-versions` → Blume's native versioning, not a `navigation.selectors` dropdown: each archived version's content goes in a top-level folder under `content.root` named for its id, listed in `versions.archived` (newest first), and Blume adds the switcher, the old-version notice, and version-scoped search. Ids must start with a letter (`1.0/` → `v1.0/`, with `redirects` from the old URLs), and a version-shaped folder left out of `versions.archived` only warns (`BLUME_VERSIONS_UNCONFIGURED_VERSION`) and publishes as current content. Full reference: `docs/content/versioning.mdx` in the installed package.\n- `starlight-image-zoom` → delete (Blume zooms content images by default).\n- `starlight-links-validator` → delete (`blume validate` covers it).\n- Anything else → report.\n\n## Assets\n\nStarlight co-locates images in `src/assets/` with **relative** references (`../../assets/foo.png`) or `astro:assets` imports; Blume serves `public/` at the site root with absolute URLs. Move `src/assets/*` into `public/`, rewrite relative image paths and `~/`/`@/` aliases to absolute `/…` URLs, and replace `<Image>` imports with Markdown images (or `<Frame>`).\n\n## i18n\n\nStarlight has **two** layouts:\n\n- **`root` locale** (default language at `src/content/docs/`, others in `fr/`, `de/`… subdirs) → matches Blume's `dir` parser as-is: `i18n: { defaultLocale, locales: [{ code, label }] }`, no file moves.\n- **No `root` locale** (every language in a subdir, including the default — `en/…`) → Blume expects the default locale **at the content root**, so move the default locale's files up one level. Starlight served them at `/en/…`, so add a `redirects` entry per page.\n\nDon't restate Blume defaults (`hideDefaultLocalePrefix: true`, `parser: \"dir\"` are already the defaults). Starlight's untranslated-page fallback matches Blume's `fallbackLocale` default.\n\n## Teardown\n\nRemove `@astrojs/starlight` (and plugin deps) from deps, delete the Starlight bits of `astro.config.*` **after harvesting redirects**, delete `src/content.config.ts` / `src/content/config.ts`, repoint scripts to the Blume CLI, add `blume`. A `src/content/docs/404.md` has no direct Blume equivalent — report it (Blume ships its own 404).\n\n## Dropped — report these\n\nSocials on a platform `footer.socials` lacks, badge variants, sidebar/item `attrs` + `translations`, `customCss` beyond `theme.css`, `head` entries other than meta tags and analytics scripts, a page's frontmatter `head`, `routeMiddleware`, splash layouts approximated with `mode: center` and hero blocks rebuilt as content, override components with no Blume slot, starlight-blog's tag and author pages, aside custom icons, EC frames/collapse/text markers, prev/next toggles, unmapped plugins, any `<Icon>` name with no Lucide equivalent.\n\n<!-- File: .agents/skills/blume-migrate/references/vitepress.md (Tokens: ~8249 | Category: anthropic-skill) -->\n# VitePress → Blume\n\nVitePress is a Vue/Vite static site generator. Its config is a module in `.vitepress/`, navigation is **declared in `themeConfig.sidebar`**, every `.md` page compiles as a **Vue single-file component** (so it can hold Vue components, `{{ }}`, and `<script setup>`), and its Markdown is markdown-it plus VitePress plugins. Watch for five traps:\n\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns (`BLUME_DIRECTIVE_SPACED_NAME`) only for a callout name in `.mdx`. Inside an open `:::` container, the same line closes the container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`). Directives are also MDX-only.\n- **Every page is `.md`, but most need to be `.mdx`.** A page with a callout, component, or math must be renamed, and MDX is stricter than Vue templates.\n- **URLs end in `.html` by default** (`cleanUrls: false`), so every page needs a redirect.\n- **The heading is the body `# H1`, not frontmatter.**\n- **Pages missing from the sidebar are hidden in VitePress.** In Blume they appear unless you hide them.\n\nScope: VitePress 1.x. Notes marked **2.0** cover `2.0.0-alpha.*`.\n\n## Detect\n\n- **A `.vitepress/` directory** holding `config.{js,ts,mjs,mts}` (or `config/index.*`), usually `docs/.vitepress/`. The directory that holds `.vitepress/` is the VitePress **project root**: the argument to `vitepress dev <dir>` in the scripts.\n- **A `vitepress` dependency**, often only in the docs package's `package.json`.\n- **Not VuePress**, which uses `.vuepress/` and `vuepress`/`@vuepress/*` deps (`references/vuepress.md`).\n\n**Where the Blume project goes.** VitePress has a project root, a `srcDir` (the pages, default `.`), and a public directory that is always `<srcDir>/public`. Blume serves `public/` from the directory holding `blume.config.ts`. Two layouts are common:\n\n- The scripts run `vitepress dev docs` from the repo root. Put `blume.config.ts` at the repo root; the default `content.root: \"docs\"` matches (append `srcDir` if one is set). Move `docs/public/` to `public/`.\n- `docs/package.json` runs `vitepress dev`. Put `blume.config.ts` in `docs/`, set `content.root: \".\"`, and scope `include`/`exclude` (`references/monorepo.md` §1). `public/` stays put, but exclude it (VitePress sites keep `.md` there behind `srcExclude`), along with `node_modules`.\n\n## Run the codemod first\n\nBuild the source once (`vitepress build <root>`) and copy `.vitepress/dist` outside the repo (`<old build>` below; skip the build if the user already made one): it's your old URL list, the old heading ids, and the record of what the live site rendered, and Teardown deletes `.vitepress/`. Then, on the VitePress source directory (Blume's `content.root`), dry-run and apply the bundled codemod:\n\n```bash\nnode <skill>/scripts/vitepress-codemod.mjs <srcDir>          # report only\nnode <skill>/scripts/vitepress-codemod.mjs --write <srcDir>  # apply\nnode <skill>/scripts/vitepress-codemod.mjs --redirects <old build>  # `.html` redirects as JSON\n```\n\nIt's zero-dependency and idempotent. It converts only where the mapping is exact: containers (paired as markdown-it pairs them, see Markdown extensions), code groups, `details`, GitHub alerts, fence `[label]`s and meta, `<<<` snippet imports and `<!--@include:-->` without regions or line ranges, `.html`/`.md`/trailing-slash links backed by a page, `<Badge type text />`, `{#id}`, `[[toc]]`, and the body H1 → `title`. It renames a page to `.mdx` when the result needs MDX (partials keep their names) and makes its comments and `<br>` MDX-safe. **Everything else is a `REVIEW` line**: frontmatter keys Blume rejects, components, Vue syntax, regions and out-of-root snippets, repaired container structure, unconverted includes (including ones inside code fences, which VitePress spliced). Work through that list with the sections below; it's the migration's to-do list.\n\n## Config: `.vitepress/config.*` → `blume.config.ts`\n\nRead the whole module and the helpers it imports (sidebars, nav, per-locale configs). For **2.0**, also read directory-level `config.*` files beside the content.\n\n| VitePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `titleTemplate` | drop (report). Blume's `<title>` is `<page> - <site>`; `seo.title` replaces a page's part |\n| `lang` (not `en-US`) on a single-language site | a one-locale `i18n` block (`i18n: { defaultLocale: \"fr\", locales: [{ code: \"fr\", label: \"Français\" }] }`): it sets `<html lang>` and the UI strings, with no switcher or prefix. Use a `code` with a UI pack |\n| `head` | per entry: a known analytics snippet → **that provider's factory** from `blume/analytics` (the gtag loader + `gtag('config', 'G-…')` pair → one `googleAnalytics({ id })`; also GTM, Plausible, …); any other `<script>` → `script()` (`src`, `async`/`defer` → `strategy`, other attrs → `attributes`, inline body → `content`). A `meta` tag → `seo.metatags`, minus the tags Blume writes itself (`description`, `robots`, `og:*`, `twitter:*` card tags; the build names the setting that owns each). `link rel=\"icon\"` → a conventional favicon name in `public/`. Fonts and other tags → drop (report) |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `cleanUrls` | no setting; see \"URLs and redirects\" |\n| `rewrites` | **move each file to the path its rewrite produced**, so the URL stays put (`slug` only where moving is impossible). VitePress relative links are already written against the rewritten paths |\n| `srcDir` / `srcExclude` | `content.root` / `content.exclude`, which adds to the defaults `**/_*` and `**/.*` |\n| `outDir`, `assetsDir`, `cacheDir`, `ignoreDeadLinks`, `metaChunk`, `mpa` | drop |\n| `appearance` | `'dark'`/`'force-dark'` → `theme.mode: \"dark\"`; `false` → `\"light\"`; `true`/`'force-auto'` → omit. Blume always shows the toggle: report a forced mode |\n| `lastUpdated: true` | `lastModified: \"git\"` |\n| `sitemap.hostname` | `deployment.site`, unless the host is Vercel or Netlify (auto-detected). `sitemap.transformItems` → drop |\n| `markdown.theme` | `markdown.code.theme: { light, dark }`; VitePress's default (`github-light`/`github-dark`) is Blume's, so omit it unless changed |\n| `markdown.lineNumbers: true` | no site-wide switch: add `lineNumbers` per fence, or drop (report) |\n| `markdown.math` | delete `markdown-it-mathjax3`; see Math |\n| `markdown.container` labels, `anchor`, `toc`, `attrs`, `image`, `languages`, `languageAlias`, `headers` | drop (report). A custom `anchor.slugify` changes every anchor |\n| `markdown.config(md)` / `preConfig(md)` with `md.use(…)` | **custom markdown-it plugins: find the syntax each one adds in the content and convert every use** (to Markdown, a directive, or a component); the codemod doesn't know it. Report each plugin |\n| `markdown.codeTransformers` with `@shikijs/vitepress-twoslash` | drop the plugin: ` ```ts twoslash ` works natively |\n| `markdown.externalLinks` | VitePress opens external links in a new tab by default; `markdown.externalLinks: true` keeps that (optional) |\n| `vite`, `vue`, build hooks (`transformHead`, `transformPageData`, `buildEnd`, …) | drop (report what each did). Blume writes canonicals, OG images, `llms.txt`, and the sitemap itself; Astro integrations go in `integrations` |\n| `locales` | `i18n` (see below) |\n\n**`themeConfig`** (default theme):\n\n| `themeConfig` | Blume |\n| --- | --- |\n| `logo` (string, `{ src, alt }`, `{ light, dark, alt }`) | `logo` (string, or `{ image: { light, dark, alt } }`), file in `public/`. A monochrome SVG → `currentColor` and the string form |\n| `siteTitle: false` / a string | `logo: { image: …, text: \"\" }` / `logo.text` |\n| `nav`, `sidebar` | see the Navigation sections |\n| `socialLinks` | GitHub → `github: { owner, repo }` (it renders in the footer); known platforms → `footer.socials` (`twitter` → `x`; list under `footer` in `docs/configuration/index.mdx`); others, including custom `{ svg }` icons → `footer.links`, or drop (report) |\n| `footer` (`message`, `copyright`) | `copyright` → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); a link inside it loses its URL (report, or add it to `footer.links`); a per-locale `copyright` → a locale map. `message` → drop (report); `footer.links` takes `{ label, href }` |\n| `editLink.pattern` (`…/edit/<branch>/<path>/:path`) | `github: { owner, repo, branch }`. `<path>` leads to the **srcDir**, but Blume builds edit links from the Blume project dir, so `github.dir` is `<path>` minus `content.root` (omit when empty). A non-`github.com` origin → `github.host` |\n| `lastUpdated.formatOptions` | its date fields → `dateFormat`; `text` → drop |\n| `search` | `local` → omit. **Algolia → omit `search`** (Blume's built-in Orama), remove the DocSearch deps, and report the `appId`/`indexName`. That index is almost always DocSearch's, filled by Algolia's crawler, and Blume's sync, given an admin key, replaces the whole index on every build: never point `algolia()` at it. `search: algolia({ appId, apiKey, indexName })` from `blume/search` is for a new index Blume owns, a separate opt-in |\n| `outline` (`2`, `[2, 4]`, `'deep'`, `false`) | `toc` (`{ minHeadingLevel, maxHeadingLevel }`; `'deep'` → 2–6; `false` → `toc: false`), only when declared |\n| `aside`, `docFooter`, the `*Label` strings, `externalLinkIcon`, `carbonAds`, **2.0** `gradedContainers`, theme-package keys | drop. UI strings come from Blume's i18n packs (`i18n.ui`) |\n\n## Navigation: `sidebar`\n\nVitePress shows only what `sidebar` lists, in that order. Make the file tree match it, carrying labels, order, and collapse into `meta.ts`; reach for `navigation.sidebar` only for shapes files can't express.\n\n- **Multiple sidebars** (`{ '/guide/': [...] }`) → one **`navigation.tabs`** entry per key (`{ label, path: \"/guide\" }`, label from the `nav`). Give each tab folder a `meta.ts` `title` matching its tab label. VitePress sidebars rarely list root pages, so once those are hidden, a root route has no loose pages to show and Blume falls back to the full tree, where an untitled tab folder shows its humanized folder name.\n- **A group whose pages sit flat in one folder** (the nesting exists only in config) → **a `(group)` folder**: `docs/(fundamentals)/controllers.md` still serves `/docs/controllers`, so no URL changes and no redirects. Give it a `meta.ts` (`title`, `pages` in the listed order). A listed page from another folder can join with frontmatter `slug` keeping its old route. **Moving a page one folder deeper breaks its relative links and assets** (`./assets/x.png`, `../other.md`): re-base them, or make them root-absolute.\n- **`collapsed`**: unset → not collapsible (Blume's `flat`); `false` → `display: \"group\"` + `collapsed: false`; `true` → `display: \"group\"` + `collapsed: true`, in that folder's `meta.ts`.\n- **A group with its own `link`** → that page becomes the folder's `index` (its URL changes: redirect the old one).\n- **Item `text` that differs from the page's title** → `sidebar.label`. **`base`** → prefix the child links before mapping. External items → `navigation.featured`. `docFooterText`, `rel`, `target` → drop.\n- **Pages no sidebar lists** → `sidebar.hidden: true`. A page listed in two sections, or a cross-section link, can't repeat in filesystem nav: keep one place and report.\n\n## Navigation: `nav`\n\n- A link to a section a sidebar key backs → a `navigation.tabs` entry. **A dropdown whose items mirror that section's sidebar groups** (a mega-menu) → a plain tab: the sidebar already shows them.\n- A dropdown of pages in one section that isn't the sidebar → a tab with `items` (flat; nested groups flatten).\n- External or utility links → `navigation.actions`; a primary call to action → `navigation.cta`. A version dropdown → drop its version links (report), but move its utility links (Releases, Team, Contributing) to `footer.links` or `navigation.actions`, so pages linked only from there stay reachable.\n- `activeMatch`, `target`, `rel`, `noIcon` → drop.\n\n## Titles\n\nVitePress takes a page's heading from its first `# H1`; frontmatter `title` only sets `<title>`. Blume renders frontmatter `title` as the H1, so **a body H1 beside a `title` shows twice**; a page with no `title` takes it from the `# H1` that opens its body and shows it once. The codemod moves a plain H1 into `title` and deletes it. It reports two cases: an H1 with inline markup (move it by hand), and a frontmatter `title` that differs from the H1 (make the H1 `title`, keep the old value as `seo.title`). A home page with no H1 takes `hero.name` / `hero.text`.\n\n## Markdown extensions\n\nVuePress shares the container and fence conventions; its snippet and include syntax differ (see `references/vuepress.md`). **What Blume renders below is MDX-only unless marked `.md` OK.** Of these targets, plain `.md` handles fence titles and meta, `// [!code …]` notations, `{#id}`/`[#id]` anchors, and `<include>`; callouts, components, and math need `.mdx`.\n\n**Container pairing.** markdown-it-container scans each container's lines for its closer, so **a bare `:::` closes the outermost open container whose fence is no longer than it, and everything nested in it**. An unclosed container runs to the end of its parent (the page, for a top-level one), and a `:::` with nothing to close shows as text. Real sites have unclosed code groups that swallow the next callout, closers that close two containers, and stray closers. The codemod pairs this way, ends a code group at its last code block when other content follows inside it (a group only holds code), drops stray closers, gives a nested callout's outer fence the extra colon Blume needs, and reports every repair. Check the old build's HTML for literal `:::` to see what the live site already showed broken.\n\n| VitePress | Blume |\n| --- | --- |\n| `::: tip` / `info` / `warning` / `danger` (spaced) | `:::tip` etc., with **no space**; `::: tip` is never parsed (codemod) |\n| `::: danger STOP` | `:::danger[STOP]`. `:::danger STOP` renders without `STOP`, and `.mdx` warns `BLUME_DIRECTIVE_OPENING_TEXT` (codemod) |\n| `::: note` / `important` / `caution` | **2.0** containers → `:::note`, `:::note`, `:::danger` (`:::warning` with `gradedContainers`). On **1.x** they aren't containers and showed as text; converting changes that: report (codemod) |\n| `::: details Title` (`{open}`) | `<Expandable title=\"Title\">` (`defaultOpen`); untitled → `title=\"Details\"` (codemod) |\n| `> [!NOTE]` `[!TIP]` `[!IMPORTANT]` `[!WARNING]` `[!CAUTION]` (+ `[!INFO]`, `[!DANGER]`) | `:::note` / `tip` / `note` / `warning` / `danger` / `info` / `danger`; text after the marker → `[Title]` (codemod). Unconverted, the five GitHub markers still render as those callouts in `.mdx` (a lazy continuation included); in `.md` they're plain quotes with a literal `[!NOTE]` that warn `BLUME_MD_GITHUB_ALERT` (rename the page to `.mdx`), and `[!INFO]`/`[!DANGER]` are plain quotes in both, with no warning. The codemod leaves lazy-continuation and indented alerts for you |\n| `::: raw`, `::: v-pre` | remove the wrapper lines (codemod) |\n| `::: code-group` + `[label]` fences | `<CodeGroup>`; each block's title is its label (bare token, or `title=\"Two words\"`), else its language as VitePress showed. Blume syncs same-titled tabs page-wide (codemod) |\n| Raw `<div class=\"tip custom-block\">…</div>` (hand-written container markup) | the matching directive |\n| ` ```js{4} ` / ` ```ts:line-numbers ` / ` ```js [file.js] ` | ` ```js {4} ` (Blume reads it glued too) / ` ```ts lineNumbers ` (`=N` start → drop) / ` ```js file.js ` (a bracketed title also shows without its brackets; `.md` OK; codemod) |\n| `// [!code ++]` `--` `focus` `highlight`/`hl` `error` `warning` | same (`.md` OK) |\n| `js-vue` | plain `js`; resolve its `{{ }}` by hand |\n| `<<< @/path/file.ts{2,4} [Label]` (`{lang}`, `:line-numbers`) | `<include meta=\"Label {2,4}\">/path/file.ts</include>` (`lang=\"…\"`, `lineNumbers`); `@` is the srcDir, `/` Blume's content root. In a code group VitePress titles an unlabeled import with its file name. A `.md` target gets `lang=\"md\"`: VitePress showed it as code, Blume would splice it (`.md` OK; codemod) |\n| `<<< file#region` | no region selection in `<include>`: generate the excerpt with `<skill>/scripts/include-excerpts.mjs` (selects `#region` markers; set `\"dedent\": true`, since VitePress always dedents regions; runs before every build, so the excerpt keeps tracking the source). Where VitePress couldn't find the region it showed the whole file, so check the old build |\n| a snippet outside the content root (`@/../packages/…`) | `BLUME_INCLUDE_OUTSIDE_ROOT` fails the build: copy the file under `_snippets/` and report |\n| `<!--@include: ./part.md-->` | `<include>./part.md</include>` (`.md` OK; codemod). A line range or region → split the partial. VitePress publishes a partial as a page unless `srcExclude` hides it: prefix partials with `_` (excluded by default) and update the paths. A partial is parsed in the including page's format, so one spliced into `.mdx` must be MDX-safe |\n| `[[toc]]` | delete; Blume has the outline rail (codemod) |\n| `:tada:` emoji shortcodes | paste the emoji. A trailing emoji leaves no dash in the id (`## Emoji 🎉` → `#emoji`); the anchor script pins any old id that differs |\n| `## Heading {#id}` | `[#id]` in both formats; a bare `{#id}` fails `.mdx` (codemod) |\n| markdown-it-attrs (`[link](/x){target=\"_self\"}`, `{.class}`) | delete (codemod, for links) |\n| `$…$` / `$$…$$` (`markdown.math`) | `$$…$$` in `.mdx`, block or inline; convert each inline `$…$`, checking it's math |\n| ` ```mermaid ` (`vitepress-plugin-mermaid`) | as written in `.mdx`; delete the plugin |\n| `:::tabs` + `== Label` (`vitepress-plugin-tabs`) | `<Tabs>` of `<Tab title=\"Label\">`, or `<CodeGroup>` when every tab is one fence |\n| Containers a site registers itself (markdown-it-container in `markdown.config`, **2.0** `customContainers`) | read the registration, then map each to a callout, `<Tabs>`, or a component (report). The codemod pairs them as containers and leaves them as written, lengthening an enclosing callout's fence: Blume ends a `:::` callout at a `::: name` line inside it and drops the line |\n\n## Vue in Markdown\n\nVitePress pages are Vue templates; Blume pages aren't. **In `.mdx` these fail the build**: an HTML comment, `{{ … }}`, a `:prop=` binding, `<script setup>`, an unclosed `<br>`/`<img>`, an `<https://…>` autolink, and `{name}` in prose. **In `.md` they pass through as text or raw HTML** with no error. The codemod reports each.\n\n- **`{{ expr }}`, `$frontmatter`, `$params`** → substitute the value; shared values → Blume `variables`; literal braces → inline code. Vue directives on HTML (`v-if`, `v-for`, `v-pre`) → static content.\n- **`<!-- comments -->`** → `{/* … */}` in `.mdx` (the codemod converts one-line ones).\n- **`<script setup>`, `<script>`, `<style>`** → delete; move styles to a root `theme.css`.\n- **`<Badge type text />`** → `<Badge variant>text</Badge>` (`tip`, the default → `accent`; `info` → `default`; `warning`, `danger` as named; **2.0** `note` → `default`, `important` → `accent`, `caution` → `danger`). Blume leaves a badge's text out of the heading's id (`## Title <Badge>beta</Badge>` → `#title`), as VitePress did for a self-closing badge, but VitePress put a badge's children in it (`<Badge type=\"info\">beta</Badge>` → `#title-beta`), so the codemod pins each converted badge heading to its VitePress id (`[#title-beta]`; `[#title]` for a self-closing badge, redundant but harmless). Unconverted in `.mdx`, a `<Badge type text />` renders empty and warns `BLUME_UNKNOWN_PROP`.\n- **Components** (global ones from the theme's `enhanceApp` or a theme package, and imported `.vue` files): decide per component: a Blume component, Markdown, a static `.astro` component in `components.ts`, or the `.vue` file as an island (`islands/`, with `@astrojs/vue vue` installed; `:users=\"['a']\"` → `users={[\"a\"]}`). Components importing from `vitepress` (`useData`, theme parts) can't run outside it. **First check the old build: a component the theme never registered rendered nothing** (`<!---->` in the HTML), so converting it shows content readers never saw. Report it. Data-driven theme components with no source of data (API lists, contributor avatars) → drop, delete headings left empty, and report the lead-in sentences that now point at nothing.\n- **Raw `<img>`/`<figure>` with a relative `src`** → a Markdown image, which is optimized. Left as is, it works: Blume publishes a file a relative `src` names beside the page, unoptimized, and `blume validate` checks it.\n- **`<ClientOnly>`** → drop the wrapper (an island that needs `window` uses `client: \"only\"`). **`VPTeamMembers`** → a `<CardGroup>` of `<Card>`s, or drop (report).\n- **Data loaders** (`*.data.{js,ts}`) → inline the data, compute it in a custom `.astro` page, or use a content source (report). **Dynamic routes** (`[param].md` + `.paths.*`) → write one page per generated path, then delete the template.\n\n## Home page (`layout: home`)\n\n- **The site home** → `index.mdx` with `mode: custom` (no title or description rendered, so write the heading): the hero as heading, paragraph, and links; each feature as a `<Card title href icon>` in a `<CardGroup cols={3}>` (an image path or inline `<svg>` icon passes to `icon`; an emoji has no slot). For a designed landing page, `pages/index.astro` on `PageLayout`.\n- **Any other page with `layout: home`** (a team or about page using only the hero) → default doc mode: `hero.name` → `title`, `tagline` → a paragraph.\n- Hero CSS variables and theme-package keys (`testimonial`, `features` styling) → drop (report).\n\n## Frontmatter\n\n| VitePress | Blume |\n| --- | --- |\n| `title` | see Titles |\n| `description` | pass through |\n| `head` | `meta name=description` → `description` (when the page has none; VitePress didn't emit a page's `head` description beside a frontmatter one); `og:image` → `seo.image`; `link rel=canonical` → `seo.canonical`; `robots` `noindex` → `noindex: true`; `keywords` → drop; anything else → drop (report) |\n| `layout: doc` | remove |\n| `layout: page` | `mode: frame`. **If the page's only content was a component you're dropping, use the default mode** so the title renders |\n| `layout: home` | see Home page |\n| `layout: false` / a custom layout | `mode: custom`, or a custom `.astro` page (report) |\n| `sidebar: false` | `mode: center` |\n| `aside: false` / `outline: false` | `mode: wide`, or drop; `outline` levels, `aside: left` → drop (report) |\n| `prev: false` **and** `next: false` | `pagination: false`; other `prev`/`next` → drop (report) |\n| `lastUpdated: <date>` | `lastModified`; `false` → remove |\n| `titleTemplate`, `editLink`, `footer`, `navbar`, `pageClass`, `markdownStyles`, theme-package keys | drop (report), or declare with `frontmatter.extend` if a component you wrote reads them |\n\n## Icons\n\nVitePress has no page-icon system. Icons you add (tabs, `meta.ts`, cards) are kebab-case Lucide names; an inline Lucide SVG (`class=\"lucide lucide-server\"`) maps to that name (`server`).\n\n## Assets\n\n- **`<srcDir>/public/`** → `public/` beside `blume.config.ts`; root-absolute references keep working.\n- **Relative images** work as written and are optimized, **except draw.io/diagrams.net SVG exports**: the `content=\"<mxfile…>\"` attribute on their `<svg>` hides the size Astro reads, so Blume serves the file as it is and warns `BLUME_SVG_UNOPTIMIZED` at the line that embeds it. Strip that attribute to have it optimized.\n- **`.html` files under `public/`** (`/pure.html`) → keep the links.\n\n## i18n\n\nA `root` locale with others in `fr/`, `zh/` folders matches Blume's `dir` layout: `i18n: { defaultLocale, locales: [{ code, label }] }`, `code` = the folder (not `lang`), `defaultLocale` = the root's language (`en-US` → `en`). Per-locale nav labels → label maps on `navigation.tabs`/`actions`; per-locale sidebars → each locale's `meta.ts`; per-locale `title`/`description` → drop (report). Pin translated headings to their own old ids: translated pages keep their own old anchors, and the anchor script (see URLs and redirects) pairs each page with its own old page, never with the default locale's. With every locale in a folder (`en/` too), move the default up to the root and redirect `/en/…`, or set `hideDefaultLocalePrefix: false`.\n\n## URLs and redirects\n\nThe old URL list is `<old build>/**/*.html` (or the live `sitemap.xml`). Blume serves `/guide/foo` for `guide/foo.md` and `/guide` for `guide/index.md`.\n\n- **`cleanUrls: false` (the default): one exact redirect per non-index page,** `{ from: \"/guide/foo.html\", to: \"/guide/foo\" }`. `vitepress-codemod.mjs --redirects <old build>` prints them all; paste the array into `redirects`. A pattern can't express a `.html` suffix (`/:path*.html` fails validation).\n- **Don't redirect an index URL** (`/guide/`, `/guide/index.html`, `/index.html`): static hosts serve Blume's `guide/index.html` there already, and `blume dev`/`preview` redirect `/guide/` to `/guide`. `from: \"/guide/\"` takes over the page (`BLUME_REDIRECT_MATCHES_PAGE`); `from: \"/guide/index.html\"` builds (no redirect page is written over the page) but is unneeded.\n- **`cleanUrls: true`:** old URLs already match.\n- **A moved route** (undone `rewrites`, a renamed `_`-prefixed page, a numeric prefix Blume strips, a `foo.md` beside `foo/index.md`, which is one route in Blume: `BLUME_DUPLICATE_ROUTE`) → redirect both the `.html` and the clean spelling.\n- **Internal links:** the codemod rewrites `.html`, absolute `.md`, and trailing-slash links that a page backs; it reports the rest. `blume validate` reports each `.html` link left (`BLUME_BROKEN_ASSET`, naming the page's route when one matches). Fix links in config, frontmatter (`hero.actions`), and raw `<a href>` too.\n- **Anchors.** VitePress slugs headings its own way (punctuation runs → one `-`, leading digit → `_`); Blume uses `github-slugger` (punctuation dropped). On a large site hundreds of ids change (`#what-s-new` → `#whats-new`, `#tsed-core` → `#tsedcore`). After the first build, run the anchor script against the old build: `node <skill>/scripts/pin-heading-ids.mjs --old <old build>` (`--write` to apply), then rebuild and rerun it until it reports 0. By default it pins every id that changed; `--only-linked` pins only the ids content links to, for a site where hundreds change, and is a choice to report, since outside deep links to the rest then land at the top of the page. It reads the ids VitePress published, so a custom `markdown.anchor.slugify` is covered too, and it finds each old page at its own route, so add `--map` only for pages that moved. A `NOTE` names a heading it couldn't pin: pin it by hand (`## What's new [#what-s-new]`) if the heading still exists. A pin **replaces** the generated id, so run `blume validate --strict` after: any `BLUME_BROKEN_ANCHOR` left was already dead on the old site (retarget it or drop the fragment, and report it).\n- **Verify** in `dist/`: every old URL is either a built page (`dist/<route>/index.html`) or a `from` in `dist/_redirects` / `dist/vercel.json` whose target is a built page. Static builds also write a meta-refresh page at `dist/<route>.html/index.html`. `blume preview` serves both, applies the redirects as the host files do, and redirects `/<route>/` to `/<route>`, so you can click through old URLs there. On Netlify, set `deployment: netlify({ output: \"static\" })`: only its `_redirects` forces the rules, so a build for no named host gets the meta-refresh page with a 200. How GitHub Pages serves `/<route>.html` is unverified, so check a few old URLs after deploying there.\n\n## Teardown\n\n- Remove `vitepress`, its plugins (`vitepress-plugin-*`, `@shikijs/vitepress-twoslash`, `markdown-it-*`), the theme package, and `vue` (keep `vue` + `@astrojs/vue` for Vue islands). Delete `.vitepress/` (once its `dist/` is copied out), data loaders, `.paths.*` files, and PostCSS/Tailwind configs that only served the theme. Theme CSS worth keeping → a root `theme.css` against Blume's tokens (`--vp-c-brand-1` → `theme.accent`).\n- **Check the `tsconfig.json` in the Blume project dir**: Blume's build reads it, so drop `.vitepress` includes and make sure `extends` resolves from that dir (install the root workspace first), or delete it if it only served VitePress. An unresolvable `extends` fails the build (`BLUME_TSCONFIG_EXTENDS`, at the line that names it).\n- Repoint `docs:dev`/`docs:build`/`docs:preview` to `blume dev`/`build`/`preview`. Grep the repo for `.vitepress/dist` and `vitepress build`: CI and deploy configs (Pages workflows, publish scripts, `netlify.toml`) must now publish `dist/` and install the docs package's deps. `.gitignore`: `.vitepress/cache`, `.vitepress/dist` → `.blume`, `dist`.\n- A `404.md` → delete (Blume ships a 404; `pages/404.astro` replaces it).\n\n## Dropped — report these\n\n`titleTemplate`, the footer `message` (and any link inside `copyright`), forced `appearance`, per-page `outline`/`aside`, one-sided `prev`/`next`, page-level toggles (`editLink`, `footer`, `navbar`, `pageClass`), `head` entries beyond meta tags and analytics, build hooks, Vite and custom markdown-it plugins, unsupported social links, line-number offsets and the site-wide switch, snippet regions and include ranges (now split files), copied out-of-root snippets, data-driven and unregistered theme components (and any content they hid), `VPTeamMembers`, data loaders and dynamic routes, hero styling, the DocSearch index (with its credentials), old-site anchors that were already broken (and any left unpinned under `--only-linked`), version-dropdown links, and `::: note`/`important`/`caution` text that 1.x showed literally.\n\n<!-- File: .agents/skills/blume-migrate/references/vuepress.md (Tokens: ~11036 | Category: anthropic-skill) -->\n# VuePress → Blume\n\nVuePress is the Vue static site generator that VitePress succeeded. Its config is a module in `.vuepress/`, navigation is **declared in the theme's `sidebar`**, every `.md` page compiles as a **Vue single-file component**, and its Markdown is markdown-it with `::: name` containers. Most of that layer matches VitePress, so **this file covers only what differs, and sends you to a named section of `references/vitepress.md` for the rest. Keep that file open too.** Watch for six traps:\n\n- **`README.md` is a folder's index page in VuePress, but an ordinary page in Blume.** `guide/README.md` publishes at `/guide/README`, and the folder URL `/guide` has no page. Rename every `README.md` to `index.md` (or `.mdx`), then fix the links to it (see \"URLs and redirects\").\n- **Containers are written `::: tip`, with a space.** Blume renders that as literal text, and warns only for a callout name in `.mdx`; inside an open `:::` container the line closes it instead. See vitepress.md § \"Markdown extensions\".\n- **URLs end in `.html`** (`/guide/getting-started.html`). Every non-index page needs a redirect.\n- **v1 sidebar groups are collapsible unless `collapsable: false`** (note the spelling). That's the opposite of VitePress and of VuePress 2.\n- **A page's title is its frontmatter `title`, falling back to the body `# H1`, and a page often has both, with different text.** See \"Titles\".\n- **Theme components fail silently.** `<code-group>`, `<code-block>`, and `<router-link>` have hyphenated names, so they pass through as unknown HTML elements, in `.mdx` too. A code group renders every block stacked, and a router link renders with no `href`. Neither `build` nor `validate` reports them.\n\nScope: **VuePress 1** (`vuepress@1.x`, Vue 2, webpack) with the default theme or a theme that `extend`s it, in full. **VuePress 2** (`vuepress@2.0.0-rc.*`, Vue 3, `@vuepress/theme-default` 2.x) is covered in notes marked **v2**. `vuepress-theme-hope` (v2) has its common syntax under \"Themes\". For any other theme (`vuepress-theme-reco`, `vuepress-theme-plume`, an ejected `.vuepress/theme/`), inventory its components and frontmatter by hand. The v1 mappings that VuePress's own docs exercise were run end to end on them; the rest of v1 (`permalink`, `base`, `vuepress-plugin-tabs`, the blog plugin, …), the **v2** notes, and the hope section come from source and docs and haven't been run.\n\n## Detect\n\n- **A `.vuepress/` directory** holding `config.{js,ts}`, or v1's `config.yml`/`config.toml`, or v2's `config.mjs`. It's usually `docs/.vuepress/`. **v2** also reads `vuepress.config.{ts,js,mjs}` from the directory the CLI runs in. The directory that holds `.vuepress/` is the **source dir**: the argument to `vuepress dev <dir>` in the `package.json` scripts.\n- **Dependencies:** `vuepress` (both majors), the v2 betas' `vuepress-vite`/`vuepress-webpack`, or `@vuepress/*`, `vuepress-plugin-*`, and `vuepress-theme-*` packages. They're often only in the docs package's own `package.json`.\n- **Which major:** a `vuepress` version of `^1` means v1. A `2.0.0-*` version, `defineUserConfig`, `bundler: viteBundler()`, or `theme: defaultTheme({…})` means v2. A v1 config is CommonJS (`module.exports`) or `defineConfig` from `vuepress/config` (1.9+), with the theme's options under `themeConfig`. A function config (`ctx => ({…})`) may branch on `ctx.isProd`, so read both sides.\n- **Not VitePress.** VitePress uses `.vitepress/` and a `vitepress` dependency (`references/vitepress.md`).\n\n**Where the Blume project goes:** `blume.config.ts` goes in the directory the docs scripts run in (the `package.json` holding `vuepress build docs`, which may be a workspace package like `packages/docs`, not the repo root), with `content.root` set to the source dir (the default `docs` usually matches). When the scripts run `vuepress build` inside the source dir itself, use vitepress.md § \"Detect\"'s second layout. The public directory is `<source dir>/.vuepress/public` (in **v2**, the `public` option). Move it to `public/` beside `blume.config.ts`. Blume's default `exclude` (`**/.*`) already skips `.vuepress/`.\n\n## The VitePress codemod\n\nvitepress.md's codemod (`<skill>/scripts/vitepress-codemod.mjs`) also reads VuePress 1 pages. Over VuePress's own docs, with `README.md` already renamed to `index.md`, it converted containers, `details`, `v-pre`, comments, `<Badge>`s (pinning their heading anchors to the old ids, which Blume now gives them anyway), `[[toc]]`, glued fence ranges, body H1s, and links that a page backs, and listed much of the rest as `REVIEW` lines. Run it on the source dir **after** the `README.md` renames (before them, links to folder indexes come back as unbacked; it leaves `.vuepress/` alone), then correct what it treats as VitePress:\n\n- **Not reported at all:** lowercase and hyphenated tags (`<code-group>`, `<code-block>`, `<router-link>`, `<demo-1>`; `<Foo-Bar/>` is reported as `<Foo>`), `:emoji:` shortcodes, `@flowstart` blocks, `sidebar: auto` (which then fails the build), **v2** `@[code]` imports, and relative `README.md` links, which stay as written. Grep for each, and rewrite the `README.md` links to the folder (see \"URLs and redirects\").\n- **`<code-group>` and `<code-block>`** aren't converted, and a page with nothing else to convert stays `.md` (VuePress's English `guide/getting-started.md` did). Convert them, and rename the page to `.mdx`. **v2** `::: code-tabs` / `::: tabs` come back as unknown containers to convert by hand.\n- **The v2 betas' `:::: code-group` + `::: code-group-item Title`** is read as VitePress's `::: code-group`: it comes out as an empty `<CodeGroup>` followed by the items, with a removed \"stray\" closer. Convert those blocks by hand before running it.\n- **v1 badge types** `error`, `warn`, and `yellow` come out as `accent`: map them as \"Vue in Markdown\" says.\n- **A frontmatter `title` that differs from the H1** is reported for `seo.title`. In VuePress it was the sidebar text, so it goes to `sidebar.label` (see \"Titles\").\n- **`<<< @/…`** resolves `@` against the source dir, which is wrong when `vuepress` ran elsewhere. Check each import it converts or reports against the `<<<` row below.\n- **`--redirects`** reads only VitePress builds: on a VuePress build it prints `[]`, which doesn't mean no redirects are needed. Use the command in \"URLs and redirects\".\n\n## Config: `.vuepress/config.*` → `blume.config.ts`\n\nRead the whole config, including the modules it imports. VuePress sites often keep `nav` and `sidebar` in per-locale files under `.vuepress/config/`.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `description` | `title` / `description` |\n| `base` (`/repo/`) | `deployment: { base: \"/repo\" }` |\n| `head` | same as VitePress `head` (vitepress.md § \"Config\"); the `[tag, attrs, content]` format is identical. `link rel=\"apple-touch-icon\"` → copy the file to `public/apple-touch-icon.png`, which Blume detects. `manifest` and `mask-icon` links → drop with the PWA |\n| **v2** `lang` | same as VitePress `lang` |\n| `locales` | `i18n` (see \"i18n\") |\n| `dest`, `temp`, `cache`, `host`, `port`, `open`, `shouldPrefetch`, `shouldPreload`, v1 `devTemplate` / `ssrTemplate`, **v2** `templateDev` / `templateBuild`, `evergreen`, `extraWatchFiles`, `debug` | drop |\n| `patterns` / **v2** `pagePatterns` | `content.include` / `exclude`. v1's default pattern also builds `.vue` files as pages: rebuild each as Markdown or a custom `.astro` page (report) |\n| `permalink` (v1, site-wide) / **v2** `permalinkPattern` | no setting. Give each affected page a `slug` that reproduces its old URL (see \"URLs and redirects\") |\n| **v2** `public` | the directory to move to `public/` |\n| `theme` | see \"Themes\" |\n| `markdown.lineNumbers` (v1) | no site-wide switch. Add `lineNumbers` to the fences that need it, or drop it (report) |\n| `markdown.slugify` | a custom slugify changes every anchor. See vitepress.md § \"URLs and redirects\" |\n| `markdown.anchor`, `markdown.toc`, `markdown.pageSuffix`, `markdown.extractHeaders` / **v2** `markdown.headers` | drop. If `extractHeaders` added `h4`, set `toc: { maxHeadingLevel: 4 }` |\n| `markdown.externalLinks` / **v2** `markdown.links.externalAttrs` | VuePress opens external links in a new tab by default. To keep that, set `markdown.externalLinks: true` (optional) |\n| `markdown.extendMarkdown`, `chainMarkdown`, `markdown.plugins` / **v2** `extendsMarkdown` (also inside local plugins) | custom markdown-it plugins. Find the syntax each adds and convert every use, as in vitepress.md's `markdown.config` row. Report each plugin |\n| **v2** `markdown.importCode.handleImportPath` | aliases for `@[code]` paths. Apply them as you convert those imports |\n| `configureWebpack`, `chainWebpack`, `postcss`, `stylus`, `scss`, `sass`, `less`, `alias`, `define`, **v2** `bundler` | drop (report) |\n\n**Theme options** (v1 `themeConfig`; **v2** the `defaultTheme({…})` options):\n\n| VuePress | Blume |\n| --- | --- |\n| `logo` | `logo`. Move the file into `public/`. **v2** `logoDark` → `logo: { image: { light, dark } }`, and `logoAlt` → `alt` |\n| `nav` / **v2** `navbar` | see \"Navigation: `nav`\". `navbar: false` → drop (report) |\n| `sidebar` | see \"Navigation: `sidebar`\" |\n| `sidebarDepth`, `displayAllHeaders`, `activeHeaderLinks` | drop. VuePress listed a page's headings in the sidebar; Blume lists them in the outline rail (`h2`–`h3`, set by `toc`) |\n| `repo` (`owner/name` means GitHub) | `github` adds the footer mark **and** an Edit on GitHub action, so set it only where the site had edit links: v1 with `editLinks: true`, **v2** unless `editLink: false`. Otherwise keep just the mark: `navigation.repo: \"https://github.com/<owner>/<name>\"`. A GitLab or Bitbucket URL → a `navigation.actions` link; edit links drop (report) |\n| `docsRepo` / `docsBranch` / `docsDir` (edit links) | `github: { owner, repo, branch, dir }`, from `docsRepo` when it's set (then point `navigation.repo` at `repo`). `docsBranch` defaults to `master` in v1 and `main` in **v2**, so set `branch` to the real branch. `docsDir` is the repo path to the source dir, so `github.dir` is `docsDir` minus `content.root` (`packages/docs/docs` → `packages/docs`). **v2** `editLinkPattern` → read the path from it the same way |\n| `lastUpdated` | v1 turns the date on when `lastUpdated` (a label or `true`) is set in `themeConfig` **or in any `themeConfig.locales[path]`**: either → `lastModified: \"git\"`, and the label drops. **v2** shows it unless `lastUpdated: false`; map it only when the config sets it, and report the default otherwise |\n| `nextLinks: false` **and** `prevLinks: false` (v1) | `pagination: false` on every page: there's no site-wide switch. Only one of them → drop (report) |\n| `search: false` | `search: false` |\n| `algolia` (`apiKey`, `indexName`, `appId`), in `themeConfig` or, in v1, any `themeConfig.locales[path]` | omit `search` (Orama) and report the credentials. A v1 `algolia` block is almost always **DocSearch**: Algolia's crawler fills the index (the app is `BH4D9OD16A` when `appId` is missing, and `algoliaOptions.facetFilters` often marks a shared index). Its records aren't Blume's, and Blume's sync, given an admin key, replaces the whole index on every build. Never point `algolia()` at it; a new index of your own is a separate opt-in |\n| **v2** `colorMode: 'dark'` / `'light'` | `theme.mode`. `colorModeSwitch: false` → report: Blume always shows the toggle |\n| **v2** `hostname` | `deployment.site`, unless the target host is Vercel or Netlify |\n| `locales` | see \"i18n\" |\n| `smoothScroll`, `searchMaxSuggestions`, `searchPlaceholder`, `repoLabel`, `editLinkText`, **v2** `contributors`, `externalLinkIcon`, `home`, `notFound`, `backToHome`, and the other UI labels | drop. A translated UI string → `i18n.ui` |\n\n**Plugins** (`plugins`, plus the ones the theme turns on):\n\n| VuePress | Blume |\n| --- | --- |\n| `@vuepress/plugin-search` (v1's built-in search), **v2** `search`, `slimsearch`, `flexsearch`, `orama` | omit: Orama is the default |\n| **v2** `@vuepress/plugin-meilisearch` | omit, and report: Blume has no Meilisearch adapter |\n| **v2** `@vuepress/plugin-docsearch` | as the `algolia` row: omit `search` and report |\n| `@vuepress/plugin-google-analytics` (v1 `{ ga }`, **v2** `{ id }`) | `analytics: [googleAnalytics({ id })]` from `blume/analytics`. A `UA-` id stopped collecting when Google retired Universal Analytics, so ask for the GA4 `G-` id, or drop it (report) |\n| Other analytics plugins | the matching `blume/analytics` adapter, or `script()` |\n| `vuepress-plugin-sitemap`, **v2** `@vuepress/plugin-sitemap` / `plugin-seo` (`hostname`) | `deployment.site` (with the same host rule as above). Blume writes the sitemap, canonicals, and Open Graph tags itself |\n| `@vuepress/plugin-medium-zoom` | omit: Blume content images zoom on click by default |\n| `@vuepress/plugin-back-to-top` | omit: Blume's page actions include \"Scroll to top\" |\n| `@vuepress/plugin-pwa` | drop (report). Blume has no service worker. The `manifest` `head` link drops with it |\n| `@vuepress/plugin-last-updated` | `lastModified: \"git\"` |\n| `nprogress`, `active-header-links`, `smooth-scroll`, **v2** `copy-code`, `git` | drop |\n| `@vuepress/plugin-register-components` | see \"Vue in Markdown\" |\n| `container` (`vuepress-plugin-container`) / **v2** `@vuepress/plugin-markdown-container` | containers the site registers. Read each entry's `type`, `before`, `after`, and `defaultTitle`, then map it as in vitepress.md's last \"Markdown extensions\" row (report) |\n| **v2** `@vuepress/plugin-redirect` | its `config` map → `redirects`. `redirectFrom` frontmatter → one `redirects` entry per old path, pointing at that page. A page with `redirectTo` → delete it and add a redirect |\n| `@vuepress/plugin-blog`, `vuepress-plugin-blog` | `type: blog` pages plus a hand-written index (see `docs/advanced/blog.mdx`). Tag, category, and pagination pages → drop (report) |\n| Mermaid, math, tabs, flowchart, and include plugins | see \"Markdown extensions\" |\n\n**Styles:** v1's `.vuepress/styles/palette.styl` `$accentColor` (or **v2** `--vp-c-accent` in `.vuepress/styles/index.scss`) → `theme.accent`. Move other rules worth keeping from `index.styl` / `index.scss` to a root `theme.css`, written against Blume's tokens (report).\n\n## Navigation: `sidebar`\n\nThe rules in vitepress.md § \"Navigation: `sidebar`\" apply: multiple sidebars become tabs, a group whose pages sit flat beside other groups' pages becomes a **`(group)` folder** (`guide/(advanced)/frontmatter.md` still serves `/guide/frontmatter`, so no redirects; list it in the parent `meta.ts` `pages` by its bare name, `advanced`), and pages no sidebar lists need a decision. The item formats differ:\n\n- **Multiple sidebars** (`sidebar: { '/guide/': [...], '/': [...] }`, fallback key last) → one `navigation.tabs` entry per non-root key, as in VitePress. Items inside a key are relative to it: `''` is the key's `README.md`, and `'getting-started'` is `/guide/getting-started.html`.\n- **Items** are page paths, with `.md` optional, or `[path, 'Text']`. A `Text` that differs from the page's title → that page's `sidebar.label`.\n- **v1 groups** (`{ title, path, collapsable, sidebarDepth, children, initialOpenGroupIndex }`) → a folder (or `(group)` folder) with a `meta.ts` (`title`, `pages` in order). `path` → the folder's `index` page. `collapsable` unset or `true` → `display: \"group\"`. Blume's groups then start collapsed except the one holding the current page, as in v1 (which also opened the first group, or `initialOpenGroupIndex`, on a page no group held). When every group collapses, set `navigation.sidebar.display: \"group\"` once. `collapsable: false` → keep the default `flat`. `sidebarDepth` and `initialOpenGroupIndex` → drop.\n- **v2 groups** (`{ text, link, prefix, collapsible, children }`, where strings are file paths such as `'foo.md'` or `'/bar/README.md'`, resolved against `prefix`) → the same, except `collapsible: true` is the only collapsible case.\n- **`'auto'`** (v1, as a key's value or the whole `sidebar`) and **v2** `'heading'` (the default) showed only the current page's headings. Blume shows the page tree instead, with the headings in the outline rail. Report that the sidebar changed.\n- **Pages reachable only from the navbar** (common: `config/`, `faq/`, `miscellaneous/` with `sidebar: auto`) → give each section a place in the tree: a tab for a top-level navbar link, or a folder for a dropdown group. Use `sidebar.hidden: true` only for pages that were truly unlisted.\n\n## Navigation: `nav`\n\nMap it as in vitepress.md § \"Navigation: `nav`\". v1 items are `{ text, link }`, or `{ text, items }` for a dropdown; a dropdown can hold `{ text, items }` subgroups, which flatten. **v2** items are `{ text, link }`, `{ text, children, prefix }`, or a page path string. Strip `.md` and `.html` from internal `link`s, and turn `README.md` into its folder. `ariaLabel`, `target`, `rel`, and `activeMatch` → drop.\n\n**A dropdown whose pages span several folders** (VuePress's own \"Learn More\": `api/`, `faq/`, `miscellaneous/`, and an external changelog) fits no single tab. Make it a tab with `path: \"/\"` and `items` for the pages (on those routes the sidebar lists the folders no other tab claims, which here are exactly those, though the tab also highlights on the home page), and move its external links to `navigation.actions`. Or, for a few pages, use `navigation.featured`.\n\n## Titles\n\nVuePress names a page with its frontmatter `title`, falling back to a heading on the page's first line (normally the `# H1`). That name is the sidebar text and the `<title>`, while the H1 still renders as written. v1's `metaTitle` overrides the whole `<title>`. Blume renders frontmatter `title` as the H1. On every page:\n\n1. Move the H1 text into `title` and delete the H1 line. Keep only the text of an H1 that carries a link or a `<Badge>`: move the link into the body, and the badge to `sidebar.badge`.\n2. A frontmatter `title` that differs from the H1 → `sidebar.label`, since it was the sidebar text.\n3. `metaTitle` → `seo.title`, minus VuePress's ` | Site` suffix: Blume adds ` - <site title>` itself. A `metaTitle` without that suffix was the whole `<title>`: keep it as written and report that the site name is now appended.\n\nA page with no H1 and no `title` took the site title. Give it a real one. **VuePress gave the H1 an id, but Blume's title has none**, so a link to it (`using-a-plugin.html#using-a-plugin`, meaning the top of the page) fails `validate` with `BLUME_BROKEN_ANCHOR`: drop the fragment.\n\n## Markdown extensions\n\nEverything Blume renders from these is MDX-only unless vitepress.md marks it `.md` OK, so a page that needs a callout or component is renamed to `.mdx`.\n\n| VuePress | Blume |\n| --- | --- |\n| `::: tip` / `::: warning` / `::: danger` (v1 default theme). **v2** adds `info`, `note`, `important`, and `caution` | same as VitePress's containers, with the space removed and a title moved into `[Title]`. **v2** `important` → `:::note` (Blume's alias), `caution` → `:::danger`. v1 has no `note` or `info` container: unless a plugin registers one, the live site showed those lines as text, so converting them changes the page (report) |\n| `::: details Title` | same as VitePress: `<Expandable title=\"Title\">`, or `title=\"Details\"` when untitled. VuePress has no `{open}` |\n| `::: v-pre` | same as VitePress's `::: raw` row |\n| `::: slot name` (v1 Markdown slots) | no equivalent. Put the content where the layout showed it, or drop it (a home page's `::: slot footer` → see \"Home page\") (report) |\n| `<code-group>` + `<code-block title=\"YARN\">` (v1, 1.6+) | `<CodeGroup>` with blank lines inside, each block's `title` becoming its fence's title (` ```bash YARN `, or `title=\"Two words\"`). `active` on a later block → move that block first (Blume opens the first tab), or use `<Tabs defaultTabIndex={n}>` with one fence per `<Tab title>`. Inside a list item, indent `<CodeGroup>`, its fences, and the closing tag to the item's content column, or the list breaks. Unconverted, the blocks render stacked with no warning |\n| `<CodeGroup>` + `<CodeGroupItem title>`, or `:::: code-group` + `::: code-group-item Title` (**v2** betas) | the same. Unconverted in `.mdx`, `CodeGroupItem` fails the build, and the spaced containers show as text |\n| `::: code-tabs#id` + `@tab Label` / `@tab:active Label` (**v2** default theme, hope) | `<CodeGroup>`, each `@tab` label becoming the title of the fence that follows it (VuePress rendered only that first fence). `#id` → drop: Blume already syncs same-titled tabs across the page. `:active` → as for `<code-block active>` |\n| `::: tabs#id` + `@tab Label` (**v2**, hope) | `<Tabs>` of `<Tab title=\"Label\">`. `#id` → `syncKey=\"id\"`, `@tab:active` → `defaultTabIndex={n}` (zero-based), and `@tab Label#value` → keep only `Label` |\n| `:::: tabs` + `::: tab \"Label\"` (`vuepress-plugin-tabs`, v1) | `<Tabs>` of `<Tab title=\"Label\">` |\n| ` ```js{4} ` | same as VitePress: Blume reads it as written, and the codemod adds the space |\n| **v2** ` ```ts title=\"config.ts\" ` | works as written (`.md` OK) |\n| **v2** `:line-numbers`, `:line-numbers=N`, `:no-line-numbers` | the first two as in VitePress; `:no-line-numbers` → remove. **v2** turns line numbers on for every block by default; Blume has no site-wide switch (report). `:collapsed-lines`, `:v-pre`, `:no-v-pre` → remove |\n| **v2** `// [!code …]` (the `notation*` options) | same as VitePress |\n| `<<< @/path/file.js{2}` / `<<< @/file.js#region` (v1) | `<include>`, mapped as in VitePress's `<<<` rows, but **`@` is the directory `vuepress` ran in, not the source dir.** Running `vuepress build docs` from the repo root makes `@/docs/x.js` → `<include>/x.js</include>`. v1 imports take no `[label]` or `{lang}`. A region, or a file outside the content root → split or copy the file, as VitePress's rows say |\n| **v2** `@[code](./file.js)` / `@[code{3-10} js{2}:no-line-numbers](../file.js)` | `<include lang=\"js\" meta=\"{2}\">../file.js</include>`. Paths are relative to the page, like `<include>`'s. A line range (`{3-10}`) → split the file (report). Unconverted, it renders as `@` plus a link to the file, with no error |\n| **v2** `<!-- @include: ./part.md -->`, with `{start-end}` or `#region` (`@vuepress/plugin-markdown-include`, md-enhance, hope) | same as VitePress's `<!--@include:-->` row. Unconverted in `.md` it stays an HTML comment, so the content disappears silently. In `.mdx`, it fails the build |\n| `[[toc]]`, `:emoji:` | same as VitePress. VuePress left an emoji shortcode out of a heading's anchor (`## Emoji :tada:` → `#emoji`), and Blume's `## Emoji 🎉` is `#emoji` too, so the pasted emoji keeps the id |\n| `@flowstart` … `@flowend` (`vuepress-plugin-flowchart`) | renders as literal text. Redraw it as a ` ```mermaid ` flowchart (`.mdx`) or an image (report) |\n| **v2** `> [!NOTE]` GitHub alerts (with the hint plugin's `alert` on, or in hope) | same as VitePress's alerts row |\n| Math, ` ```mermaid `, markdown-it-attrs (`{#id}`, `{.class}`) | same as VitePress's rows. v1 and v2 core have no `{#id}` or attrs; only plugins (hope's `attrs`) add them |\n\n## Vue in Markdown\n\nvitepress.md § \"Vue in Markdown\" applies: the `.mdx` build errors, `{{ }}`, `<script>`/`<style>` blocks, comments, `<ClientOnly>`, directives, and the per-component decision. VuePress adds:\n\n- **Globals:** `$page`, `$site`, `$frontmatter`, `$themeConfig`, `$lang`, `$localePath`, `$title`, `$description`, and `$style` (from `<style module>`) → substitute or delete. `$withBase('/img.png')` → the plain root path (`![alt](/img.png)`): Blume adds `deployment.base` to internal links and assets.\n- **`<Badge>`** → Blume `<Badge>`, with the text as **children**. v1 `type`: `tip` (the default) or `green` → `accent`; `warning`, `warn`, or `yellow` → `warning`; `error` → `danger`. **v2** `type`s map as in vitepress.md. `vertical` → drop. An unconverted badge renders empty in `.mdx`, which warns `BLUME_UNKNOWN_PROP`, and as nothing in `.md`, with no warning. **Anchors:** VuePress left a badge out of a heading's anchor (`## Custom Containers <Badge text=\"default theme\"/>` → `#custom-containers`), and so does Blume, so the converted heading keeps its id.\n- **`<router-link to=\"/x.html\">`** (v1), `<RouterLink>`, and **v2** `<RouteLink>` → a Markdown link to the route (`[text](/x)`). `<OutboundLink/>` and `<Content/>` → delete.\n- **Global components:** v1 registers every `.vuepress/components/**/*.vue` file globally, named after the path (`Foo/Bar.vue` → `<Foo-Bar/>`, `demo-1.vue` → `<demo-1/>`), plus theme `global-components` and `Vue.component()` calls in `.vuepress/enhanceApp.js`. **v2** registers them through `@vuepress/plugin-register-components` or `.vuepress/client.{js,ts}`. Decide each as in VitePress. A hyphenated tag passes through as an unknown HTML element, so give the Blume component a PascalCase name and rename its tags (`<demo-1/>` → `<Demo1 />`). **v1 components are Vue 2**, but `@astrojs/vue` runs Vue 3, so an island needs a Vue 3 port: functional components, filters, `$listeners`, `.sync`, and `this.$set` all changed (report). A component that reads `$page`, `$site`, `$themeConfig`, or `this.$router` can't run outside VuePress: rewrite it or drop it.\n- **An escaped `\\<App>`** (VuePress's `api/node` headings) renders as text in `.mdx`, with no warning, so it can stay as written.\n- **`.vuepress/enhanceApp.js` / v2 `client.{js,ts}`** (Vue plugins, router hooks, global components) → drop, and report what each did. **v2** client-config `layouts` → custom `.astro` pages.\n\n## Home page (`home: true`)\n\nRebuild it as in vitepress.md § \"Home page (`layout: home`)\": `index.mdx` with `mode: custom`, or a designed `pages/index.astro`. It's usually the source dir's `README.md` (or `index.md`). VuePress's keys:\n\n- v1: `heroImage`, `heroAlt`, `heroText` (it defaults to the site title, and `null` hides it), `tagline` (it defaults to the site description, and `null` hides it), `actionText` + `actionLink` (one button), `features: [{ title, details }]`, and `footer`. **v2:** `actions: [{ text, link, type }]` replaces the single action, and adds `heroImageDark`, `heroHeight`, and `footerHtml` (which renders `footer` as HTML).\n- `mode: custom` is full-bleed with no padding, so a hero image renders huge and flush left. Wrap the hero in a centered block (`<div style={{ maxWidth: \"60rem\", margin: \"0 auto\", padding: \"3rem 1.5rem 0\", textAlign: \"center\" }}>`, with blank lines inside so the Markdown renders) and cap the image (`style={{ maxHeight: \"280px\", margin: \"0 auto\" }}`). Add `sidebar.hidden: true` so the home page isn't a sidebar row.\n- Features have no icon or link: each becomes a `<Card title>` with `details` as the body, in a `<CardGroup cols={3}>`. Features written as raw HTML in the body (`<div class=\"features\">`) map the same way.\n- `footer` (HTML with **v2** `footerHtml: true`) or a `::: slot footer` is usually a copyright or license line (\"MIT Licensed | Copyright © 2018-present …\") → `footer.copyright`, plain text: strip the tags, decode entities (`&copy;` → `©`); move its links into `footer.links`, or they lose their URLs (report). A translated home page's `footer` → that locale's entry in a locale map. Other footer content → drop (report). `heroImageDark` → Blume has no light/dark pair for a content image, so keep one (report).\n\n## Frontmatter\n\nBlume's schema is strict: every key below must be mapped or removed.\n\n| VuePress | Blume |\n| --- | --- |\n| `title` / `metaTitle` | see \"Titles\" |\n| `description` | pass through |\n| `meta` (v1) / **v2** `head` | per entry, as in vitepress.md's frontmatter `head` row |\n| `canonicalUrl` (v1.7.1+) | `seo.canonical` |\n| `lang` | remove: Blume sets `<html lang>` per locale |\n| `permalink` | `slug`, the old path without its leading `/`, trailing `/`, or `.html` (see \"URLs and redirects\") |\n| `layout` naming a `.vuepress/components` or theme component | `mode: custom`, and rebuild the content, or use a custom `.astro` page (report). `layout: Layout` → remove |\n| `home` and the hero keys | see \"Home page\" |\n| `sidebar: auto` / **v2** `sidebar: heading` | remove: the outline rail lists the headings |\n| `sidebar: false` | `mode: center` |\n| **v2** `sidebar` as an array (a per-page sidebar) | remove (report) |\n| `sidebarDepth` | remove |\n| `prev` / `next` | same as VitePress |\n| `search: false` (v1) | remove (report): it hid the search box on that page, which stayed searchable |\n| `tags` (v1, extra search terms) | `search.keywords` |\n| **v2** `date` | keep on `type: blog` pages, otherwise remove |\n| **v2** `redirectFrom` / `redirectTo` | `redirects` (see the redirect plugin row) |\n| `navbar`, `pageClass`, `editLink`, **v2** `editLinkPattern`, `lastUpdated`, `contributors`, `externalLinkIcon`, `permalinkPattern`, `routeMeta` | drop (report) |\n| keys a theme reads | see \"Themes\" |\n\n## Themes\n\n- **A theme that `extend`s `@vuepress/theme-default`** (v1's `@vuepress/theme-vue`, or a local `.vuepress/theme/index.js` with `extend`) → treat it as the default theme. Layouts it overrides (ads, banners) → drop (report).\n- **An ejected or local theme** (a full `.vuepress/theme/` with `Layout.vue`, `Navbar.vue`, …) → map the `themeConfig` keys it shares with the default theme. Any extra key (a version list inside the sidebar, say) is custom, so read its components to see what it rendered (report).\n- **`vuepress-theme-hope`** (`hopeTheme({…})`, often in `.vuepress/theme.ts`):\n  - **Sidebar** `\"structure\"` builds the tree from files, which Blume does by default. `order` → `sidebar.order`, `index: false` → `sidebar.hidden: true`, and `shortTitle` → `sidebar.label`. Hope sorts positive `order`s first, then unordered pages by title, then negative `order`s (`-1` last), while Blume sorts a negative order first: renumber, or list the folder's order in its `meta.ts` `pages`. A folder `README.md`'s `dir` → that folder's `meta.ts`: `text` → `title`, `icon`, `order`, `collapsible` (on by default) → `display: \"group\"`, and `expanded: true` → `collapsed: false`. `dir.index: false` (the folder left out of the generated sidebar) → `sidebar.hidden: true` on each of its pages.\n  - **Icons** (frontmatter, navbar, sidebar) are FontAwesome or Iconify names → Lucide (SKILL.md \"Icons are Lucide\").\n  - **Markdown** (the theme's `markdown` options, or `plugins.mdEnhance`): `^sup^`, `~sub~`, task lists, and footnotes work as written. `==mark==` → `<mark>…</mark>`. `::: center` / `::: right` → drop the alignment. `!!spoiler!!` → drop. `<VPCard>` → `<Card>`. A ` ```component Name ` fence (YAML or JSON props) → that component with the props as attributes. `<Catalog />` → `directory: \"card\"` in that folder's `meta.ts`. `<!-- more -->` → delete. Charts (Chart.js, ECharts, Markmap, PlantUML, flowchart), playgrounds, `::: demo`, and slides → no equivalent: a mermaid diagram, an image, or a link (report).\n  - **Frontmatter:** `author` and `date` → `authors` and `date` on `type: blog` pages, otherwise drop. `category` and `tag` → drop (report: Blume has no tag pages), or `search.keywords`. `article`, `star`, `sticky`, `cover`, `isOriginal`, `pageInfo`, `breadcrumb`, `toc`, `comment`, `copy`, `footer`, `copyright`, `containerClass`, and any other hope-only key (`license`, `pageview`, `backToTop`, `sitemap`, `feed`, …) → drop. The project home's `highlights` → more cards. `bgImage` and the hero style keys → drop (report).\n\n## Icons\n\nThe default theme has no page icons. Icons you add to tabs, `meta.ts`, cards, or `sidebar.icon` are kebab-case Lucide names. Hope's icons map as above.\n\n## Assets\n\n- **`.vuepress/public/`** → `public/`. Root-absolute references (`/hero.png`) keep working.\n- **Relative images** (`./img.png`) work as written.\n- **Webpack paths** (v1 `~@alias/img.png`, `~some-package/img.png`, the `@source` alias) → copy the file into the content root or `public/` and link it plainly.\n\n## i18n\n\nMap it as in vitepress.md § \"i18n\". VuePress's `locales` keys are paths (`'/zh/'`), so the `code` is the folder (`zh`), not the `lang` (`zh-CN`). The default locale's `lang` (`en-US`) gives `defaultLocale` (`en`). The theme's per-locale options (v1 `themeConfig.locales`, **v2** the theme's `locales`) carry the rest:\n\n- `label` (**v2** `selectLanguageName`) → that locale's `label`. `selectText` (**v2** `selectLanguageText`), `ariaLabel`, `editLinkText`, the `lastUpdated` text, `serviceWorker`, and the other UI labels → drop, or `i18n.ui`.\n- `nav` → per-locale label maps on `navigation.tabs` / `actions`.\n- `sidebar` (keyed `'/zh/guide/'`, …) → each locale's own folder `meta.ts` files.\n- A per-locale container title (`defaultTitle: { '/zh/': '提示' }`) → drop: Blume's UI packs (including `zh`) title callouts.\n- A per-locale `title` / `description` → drop; a translated home page can keep its description in frontmatter.\n- **Anchors:** keep each locale's own old ids (`/zh/` pages had Chinese slugs, which zh pages and outside zh links use). The anchor script does this, pairing every page with its own old page. vitepress.md's \"Pin translated headings\" means exactly that, not pinning to the default locale's ids.\n\n## URLs and redirects\n\nFollow vitepress.md § \"URLs and redirects\", treating VuePress like `cleanUrls: false`: one exact redirect per non-index `.html` page, never a redirect from an index URL, both spellings for a moved route, and rewritten internal links. VuePress specifics:\n\n- **Build the old site first and keep its output** (`<source>/.vuepress/dist`, or `dest`): it's the URL list and the record of the old anchors. VuePress writes no sitemap unless a plugin adds one. v1 runs on webpack 4, which fails on current Node with `error:0308010C:digital envelope routines::unsupported`: build it with `NODE_OPTIONS=--openssl-legacy-provider`, or under Node 16 (`npx -p node@16 …`).\n- **Redirects:** run this in that output directory and paste the result into `redirects` (the VitePress codemod's `--redirects` reads only VitePress builds):\n\n  ```bash\n  find . -name '*.html' ! -name index.html ! -name 404.html | sed 's#^\\.##' | sort \\\n    | awk '{ to = $0; sub(/\\.html$/, \"\", to); printf \"  { from: \\\"%s\\\", to: \\\"%s\\\" },\\n\", $0, to }'\n  ```\n\n- **`README.md` → `index.md`** keeps the folder URL (`/guide/` is served from `guide/index.html`), so it needs no redirect. Rewrite each link to a `README.md` to its folder (`./README.md` → `./`, `/guide/README.md` → `/guide`). Once the file is renamed, `blume validate` flags the ones you miss.\n- **`permalink` pages** served the permalink, not the file path. Set `slug` to it. A permalink ending in `.html` also needs a redirect from that `.html` URL.\n- **Links:** VuePress authors link to `.md` files, relative or absolute (`/guide/foo.md`; **v2** recommends relative), and to `.html`. Rewrite them as vitepress.md says. Also fix links in `nav`, `sidebar`, `actionLink`, hero `actions`, and raw `<a href>`.\n- **Anchors: pin them with the bundled script.** VuePress slugs headings as VitePress does, so ids change on many pages (115 headings on 32 of 92 pages in VuePress's own docs, counted before Blume left badges and trailing emoji out of its ids). **Duplicate headings number differently:** VuePress's second `## Options` is `#options-2`, Blume's is `#options-1`. After `blume build`, run from the Blume project:\n\n  ```bash\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist>          # dry run\n  node <skill>/scripts/pin-heading-ids.mjs --old <old .vuepress/dist> --write  # apply\n  ```\n\n  It pairs each page's headings with the old build's by text and position, translated pages with their own old pages, and appends `[#old-id]` (`{#old-id}` in `.md`) where the ids differ. Rebuild and rerun until it reports 0. A `NOTE` names a heading whose source line it couldn't match, such as one with inline code, a Markdown escape (`\\<`), or `--` (19 of the 115 in VuePress's docs): pin those by hand. It also pins the home hero's `#main-title`, which is harmless. On a large site, `--only-linked` pins only the ids something links to.\n\n- **`validate` also reports anchors that were already broken.** VuePress never checked fragments (a zh page linking an English id, a renamed section). Point each at the right heading or drop the fragment, and report it.\n- **Hosts:** Netlify (with pretty URLs) and GitHub Pages also answered `/guide/foo` without `.html`, which Blume keeps. On Netlify, a static build for no named host writes unforced `_redirects` rules, so Netlify serves the meta-refresh page instead of a 301: use `deployment: netlify({ output: \"static\" })` (see Redirects in `docs/02-deployment.mdx`). The GitHub Pages caveat in vitepress.md applies.\n\n## Teardown\n\n- Remove `vuepress`, `vuepress-vite` / `vuepress-webpack`, `@vuepress/*`, `vuepress-plugin-*`, `vuepress-theme-*`, `vue`, `vue-template-compiler`, `vue-server-renderer`, and any markdown-it, Stylus, Sass, or webpack packages that only served VuePress. Keep `vue` with `@astrojs/vue` if you made Vue islands.\n- Delete `.vuepress/` (config, `config/`, `components/`, `theme/`, `styles/`, `enhanceApp.js` / `client.*`, `dist/`, `.temp/`, `.cache/`) once `public/` has moved.\n- Repoint the scripts (`docs:dev`, `docs:build`) to `blume dev` / `blume build` / `blume preview`, and add `blume`. Remove `NODE_OPTIONS=--openssl-legacy-provider`.\n- **Node:** Blume needs 22.19 or later, and v1 sites often pin an old one (`.nvmrc`, `engines`, CI `node-version: '14'`, Netlify's `NODE_VERSION`). Bump each. When the same CI job also builds or tests other packages (yarn 1 refuses to install a dependency whose `engines` excludes the running Node), the bump reaches them too, so report it.\n- Grep the repo for `.vuepress/dist`, `dest`, and `vuepress build`. Deploy scripts (the VuePress guide's `deploy.sh` pushing `docs/.vuepress/dist` to `gh-pages`), `netlify.toml` publish dirs, and CI workflows must now publish Blume's `dist/`. Settings kept in a host's dashboard (Netlify's publish directory, build command, and `NODE_VERSION`) can't be committed: list them for the user, with a full (not shallow) clone if you set `lastModified: \"git\"`.\n- `.gitignore`: replace the docs' own entries (`.vuepress/dist`, the old `dest`) with `.blume` and `dist`. Keep entries other packages in the repo still use (`.temp`, `.cache`).\n- A `404.md` → delete it (as in VitePress).\n\n## Dropped — report these\n\nEverything vitepress.md's list covers that this site used, plus: v1 Markdown slots, `@flowstart` diagrams that weren't redrawn, the PWA and service worker, `sidebarDepth` and heading-only (`auto`) sidebars, `nextLinks` / `prevLinks` when only one was off, v1 `<code-block active>` defaults that couldn't move, Vue 2 components that weren't ported, `enhanceApp` / client-config code, `.vue` pages, `pageClass` and layout components, Stylus/Sass palette overrides, Universal Analytics ids, the DocSearch index, GitLab and Bitbucket edit links, old-site anchors that were already broken, hope-only components and frontmatter, and any `note` / `info` text that v1 showed literally and now renders as a callout.\n\n<!-- File: .agents/skills/blume-migrate/SKILL.md (Tokens: ~12996 | Category: anthropic-skill) -->\n---\nname: blume-migrate\ndescription: Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, VitePress, VuePress, Docus, MkDocs, mdBook, Fern, GitBook, ReadMe, Redocly, Docsify, Jekyll (Just the Docs), GitHub Wiki, or any docs framework) to Blume, the markdown-first docs framework on Astro. Translate the source config to blume.config.ts, restructure content into Blume's filesystem-derived navigation, rewrite JSX callouts to directives, convert icons to Lucide, and inline snippets. Use when the user asks to migrate/convert/port a docs repo to Blume, or when the repo has a docs.json/mint.json, docusaurus.config.*, meta.json with fumadocs, _meta.* with nextra, an astro.config.* with starlight(), a .vitepress/config.* or .vuepress/config.*, an mkdocs.yml or zensical.toml, a book.toml (mdBook), a fern/docs.yml, a GitBook .gitbook.yaml/gitbook-docs.yaml, ReadMe _order.yaml files in docs/ or reference/, a Redocly sidebars.yaml or @redocly/realm dependency, an index.html that sets window.$docsify, a docus or @nuxt-themes/docus dependency, a Jekyll _config.yml (Just the Docs theme), or a GitHub wiki clone (Home.md with _Sidebar.md, [[Page Name]] links).\n---\n\n# Migrate to Blume\n\nBlume is a **markdown-first** documentation framework on Astro/Vite. You drop Markdown/MDX into a folder and get navigation, search, theming, Open Graph images, and a component library with no app boilerplate — **the framework is the template**. There is no starter to clone; the only thing a project owns is its content and a `blume.config.ts`.\n\nYour job is to convert a source docs repo into an **idiomatic** Blume project — not a 1:1 transliteration. Read this file, detect the source framework, open the matching `references/<framework>.md` for the exact mappings, and work the loop below. Report everything you drop or approximate.\n\nThroughout this skill (including the `references/` files), **`<skill>` means the absolute path of the directory containing this SKILL.md** — resolve it from wherever you read this file (e.g. `node_modules/blume/skills/blume-migrate` or `.claude/skills/blume-migrate`). It is a placeholder to substitute, never a literal path.\n\n## Migration philosophy\n\n- **Target idiomatic Blume, not a mechanical port.** Prefer filesystem-derived navigation over an exhaustive explicit `navigation.sidebar`. Prefer `:::` directives over JSX callouts. Prefer Blume defaults over restating them in config.\n- **Every field has a default; `{}` is a valid config.** Map only what the source _declares_. If the source uses a framework default, don't write it.\n- **Drop chrome that has no Blume equivalent — and say so.** Navbar CTAs, custom theming, conditional redirects, and unmappable icons get reported to the user, not silently discarded or faked.\n- **Convert, don't preserve.** Blume's page frontmatter schema is **strict** — unknown keys are build errors. A source-only frontmatter key must be mapped to a Blume key or removed (and reported), never left to \"maybe validate.\"\n\n## Migration workflow\n\n1. **Detect the source framework** and read its reference file:\n   - `docs.json` / `mint.json` → **Mintlify** (`references/mintlify.md`) — the deepest, config-declared nav.\n   - `docusaurus.config.*` → **Docusaurus** (`references/docusaurus.md`).\n   - `meta.json` + `fumadocs-*` deps (content under `content/docs/`) → **Fumadocs** (`references/fumadocs.md`).\n   - `_meta.{js,ts,json}` + `nextra` deps → **Nextra** (`references/nextra.md`).\n   - `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).\n   - `.vitepress/config.{js,ts,mjs,mts}` (often `docs/.vitepress/`) or a `vitepress` dependency → **VitePress** (`references/vitepress.md`). `.vuepress/` is VuePress (next bullet).\n   - `.vuepress/config.{js,ts,mjs}` (v1 also `config.yml`/`config.toml`; often `docs/.vuepress/`), a `vuepress.config.*`, or a `vuepress` dependency → **VuePress** (`references/vuepress.md`). It builds on `references/vitepress.md` for the Markdown layer the two share, so read both.\n   - A `docus` dependency (often in `docs/package.json` beside a `nuxt.config.ts` with `extends: ['docus']`), or `@nuxt-themes/docus` for 1.x → **Docus** (`references/docus.md`). Pages are MDC, Nuxt Content's component syntax: `::note` … `::` is not a Blume directive, and Blume prints it as text (`BLUME_MDC_SYNTAX` warns at its line).\n   - `mkdocs.yml`/`mkdocs.yaml` (also Zensical's `zensical.toml` and ProperDocs' `properdocs.yml`) → **MkDocs / Material for MkDocs** (`references/mkdocs.md`): nav declared in config, Python-Markdown extension syntax (`!!!` admonitions, `===` tabs, `--8<--` snippets), and Python deps, so usually no `package.json`. Run `node <skill>/scripts/mkdocs-codemod.mjs --write <content-dir>` first, then `pin-heading-ids.mjs` after the first build.\n   - `fern/docs.yml` (beside `fern/fern.config.json`) → **Fern** (`references/fern.md`). A `fern/` folder without `docs.yml` only generates SDKs: the docs are elsewhere.\n   - `.gitbook.yaml`/`.gitbook.yml`, `gitbook-docs.yaml`, or a `SUMMARY.md` beside a `.gitbook/` folder → **GitBook** (`references/gitbook.md`). `book.json` + `SUMMARY.md` is the legacy GitBook CLI or HonKit (same reference, its own section); `book.toml` + `src/SUMMARY.md` is mdBook (`references/mdbook.md`).\n   - `book.toml` (at the book root, often `docs/`, `book/`, or `guide/` in a code repo; chapters in its `[book] src`, beside a `SUMMARY.md`) → **mdBook** (`references/mdbook.md`). Every URL ends in `.html`, `SUMMARY.md` is the whole nav, and `{{#include}}` pulls anchored code from across the repo: build the old book first, since it's the URL list, the old ids, and the only record of what the includes showed.\n   - `sidebars.yaml` or an `@redocly/realm`-family dependency, usually beside a `redocly.yaml` and with Markdoc `{% … %}` tags in pages → **Redocly** (`references/redocly.md`). GitBook pages use `{% … %}` tags too, so check for GitBook's files first. A `redocly.yaml` holding only lint/bundle keys (`extends`, `rules`, `decorators`, `apis`) is Redocly CLI config, not a docs site: keep it and detect the framework from the other signals.\n   - `_order.yaml` files in `docs/`, `reference/`, `recipes/`, or `custom_pages/` (ReadMe's bi-directional Git sync), or a CI step running `readmeio/rdme` beside pages with `category`/`parentDoc` (or `category.uri`/`parent.uri`) frontmatter (`rdme` uploads) → **ReadMe** (`references/readme.md`). ReadMe keeps settings, redirects, the glossary, and variables in its dashboard, not the repo, and changelogs only on the `main` branch.\n   - An `index.html` setting `window.$docsify` (usually in `docs/`, beside `_sidebar.md`), or `docsify-cli` in `package.json` → **Docsify** (`references/docsify.md`). Run `node <skill>/scripts/docsify-codemod.mjs --write --snapshot .docsify-old --group-folders <docs-dir>` first, then `pin-heading-ids.mjs --old .docsify-old --map docsify-routes.json` after the first build. Its default URLs live after `#` (`/#/page`), which no server-side redirect can match; the reference adds a client-side script.\n   - `_config.yml` (root or `docs/`) → **Jekyll** (`references/jekyll.md`); Just the Docs is `just-the-docs` in `_config.yml` or the `Gemfile`. Build the old site first with a project-local bundle: the nav comes from front matter (`parent`/`nav_order`) and is rebuilt from the old build's sidebar, and URLs end in `/` or `.html`. A `_toc.yml` beside it is Jupyter Book, not Jekyll. Run `node <skill>/scripts/jekyll-codemod.mjs --old <old build> --write .` from the Jekyll source dir first, then `pin-heading-ids.mjs --old <old build>` after the first build.\n   - A clone of `<repo>.wiki.git` (`Home.md` at the root, often `_Sidebar.md` and `_Footer.md`, `[[Page Name]]` links, no config) → **GitHub Wiki** (`references/github-wiki.md`). Never detected: the source is named. Its old URLs stay on github.com, which can't redirect, so the reference writes a route table and link stubs for the old wiki instead of `redirects`. Run `node <skill>/scripts/github-wiki-codemod.mjs routes --repo <owner>/<repo> --write` first, classify `wiki-routes.json`, then `convert --repo <owner>/<repo> --write`.\n   - Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.\n   - **Also note the host repo, independent of source framework:** a pnpm/Turbo workspace, a non-`docs/` content layout, or a Vercel deploy each need integration steps (`content.root` scoping, `minimumReleaseAge`, lockfile, `vercel.json`, an Astro/Vite patch) — all in `references/monorepo.md`. Read it whenever the target isn't a bare single-package docs folder.\n2. **Inventory the repo** before changing anything (a page whose body is only a `<meta http-equiv=\"refresh\">` is a redirect, not a page: add the redirect from its old URL and report it): the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs and GraphQL schemas, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted. Pages the old site published by accident (a `.github/` template, files under dot-folders, planning notes) aren't content: leave them out, add no redirects, and list them in the report.\n3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: \"…\" })`.\n4. **Restructure content.** Put `blume.config.ts` in the directory whose `package.json` runs the old docs build, which may be a workspace package such as `packages/docs` rather than the repo root. Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: \".\"` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** A group that exists only in the source's nav config, with its pages flat on disk, becomes a **`(group)/` folder**: it adds the sidebar group and keeps every URL, with no redirects (moving pages a folder deeper instead changes their URLs and breaks their relative asset paths). Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.\n5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; turn snippets/partials into `<include>` statements or inline them (Blume has no import-based includes: `import Snippet from \"…\"` plus `<Snippet />` has to become one or the other; when a source includes part of a file (mdBook `ANCHOR`s, VitePress `#region`s, MkDocs snippet line ranges) or a file outside the content root, copy `<skill>/scripts/include-excerpts.mjs` into the project and run it before every `dev`, `build`, and `validate` rather than pasting code the project compiles: it reports every missing anchor or region in one pass, and a restored marker in the source beats a line range, which goes stale silently); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources — and links inside OpenAPI descriptions, which Blume renders and validates, though a redirect `from` hides a stale one; rewrite links that hardcode the old site's own origin to root-relative paths); **add a `redirects` entry for every route you moved** in step 4 (a hash-routed source like Docsify also needs its reference's client-side redirect, because `redirects` only see paths); **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`), and fix links to its anchor (`#<h1-slug>`), which no longer exists: drop the fragment. `blume validate --strict` also reports fragments the old site never checked; fix each or drop it, and report it. A page with no frontmatter `title` takes its opening `# H1` as the title and shows it once, so that one can stay; moving it into `title` is still the idiomatic form. **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `githubReleases()` source** (see \"Changelogs\" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`). For **Docus**, run the bundled codemod first: `node <skill>/scripts/docus-codemod.mjs --write content` pairs MDC blocks as remark-mdc does, converts the Nuxt UI prose components, fence labels, icons, frontmatter, and `.navigation.yml` files, makes the MDX prose safe, and lists every `REVIEW` item (custom and landing-page components stay as written; check them against the live site, not only a local build; see `references/docus.md`). For **VitePress**, run the bundled codemod first: `node <skill>/scripts/vitepress-codemod.mjs --write <srcDir>` converts containers (paired as markdown-it pairs them), code groups, alerts, fence labels, `<<<` imports and includes, `.html` links, badges, and H1 titles, and lists every `REVIEW` item for you to finish (see `references/vitepress.md`). Before converting any source's components, check its built output: a component its theme never registered rendered nothing there (`<!---->`), so converting it shows content readers never saw; report it. For **mdBook**, convert every `{{#…}}` helper before anything else: anchored, line-range, and out-of-root code becomes generated excerpts, one-line files become `variables`; then strip hidden `# ` lines and `rust,…` fence attributes, and convert admonish blocks (see `references/mdbook.md`). For **Jekyll**, convert every Kramdown IAL (`{: .note }`) and Liquid tag: in `.md` they ship as literal text (a Liquid tag warns `BLUME_TEMPLATE_TAG`, an IAL `BLUME_MD_ATTRIBUTE_LIST`), and in `.mdx` they fail the build (`blume check` names each IAL's line as `BLUME_MDX_ATTRIBUTE_LIST` and a Liquid tag's as `BLUME_MDX_SYNTAX`, at the partial's own line for one in an included partial). Run the bundled codemod first: it converts callout IALs, the in-page TOC, H1s, front matter, Markdown includes, and Liquid links and variables, rebuilds the Just the Docs sidebar as folders, `slug`s, and `meta.ts`, and writes the redirects and variables to `jekyll-migration.json` (see `references/jekyll.md`). For a **GitHub Wiki**, run the bundled codemod (`routes`, then `convert`): it builds the route table (one flat route per page name) and converts every `[[…]]` link outside code, which Blume would otherwise render as literal text (`BLUME_WIKILINK_UNSUPPORTED` warns only for one that names a page or has a `|`; Markdown pages put the link text first: `[[text|Page]]`). For **Redocly**, convert every Markdoc `{% … %}` tag: Blume never parses them, so an unconverted tag ships as literal text in `.md` (`BLUME_TEMPLATE_TAG` warns at its line) and fails an `.mdx` build. For **VuePress**, rename every `README.md` to `index` first, then run the VitePress codemod (its limits on VuePress content are in `references/vuepress.md`), then grep for hyphenated theme tags (`<code-group>`, `<code-block>`, `<router-link>`): they pass through `.mdx` as unknown HTML elements with no diagnostic. For **MkDocs**, decide `.md` vs `.mdx` per page before converting: MDX rejects HTML comments, `<https://…>` autolinks, unclosed void tags, and `{…}` attribute lists that Python-Markdown accepts (`blume check` names each at its line: `BLUME_MDX_SYNTAX` for what MDX can't parse, and the last two as `BLUME_MDX_UNCLOSED_ELEMENT` and `BLUME_MDX_ATTRIBUTE_LIST`), and snippet paths resolve from the snippets `base_path` (default: where MkDocs runs), not the page (see `references/mkdocs.md`). For **Fern**, URLs come from `docs.yml`, not file paths: run `node <skill>/scripts/fern-codemod.mjs --sitemap old-urls.txt` before moving anything (dry run, then `--write`), and after the first build its `endpoints` step, which joins `operation-routes.mjs` output to Fern's old API URLs and writes `fern-redirects.json`; export a Fern Definition to OpenAPI with `fern export` (see `references/fern.md`). For **GitBook**, put each page at its old URL first (GitBook's URLs follow `SUMMARY.md`, not the file tree), then convert every `{% … %}` tag: in `.md` an unconverted tag ships as literal text (`BLUME_TEMPLATE_TAG` warns), and in `.mdx` it fails the build. For **ReadMe**, run the bundled codemod first: `node <skill>/scripts/readme-codemod.mjs --write --site <hub origin> --glossary <saved hub page> .` keeps every page at its flat URL, converts callouts, `doc:`/`ref:` links (`blume validate` names any left as `BLUME_UNSUPPORTED_LINK_SCHEME`), ReadMe components, and frontmatter, writes `meta.ts`, overlays, and redirects, and lists what needs a person; after the first build, its `--routes` pass adds the endpoint redirects and per-tag order (see `references/readme.md`). A pasted widget's JSX event handler (`onClick={…}`) builds green and never runs on a static page: `BLUME_MDX_EVENT_HANDLER` names each one, so move the widget into an island (`islands/`) or drop it and report it.\n6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. Yarn's `npmMinimalAgeGate` refuses a fresh `blume` the same way: add it to `npmPreapprovedPackages`. **Always regenerate the lockfile in the same change** with the repo's own package manager (`pnpm install` from the workspace root, `yarn install`, `npm install`, or `bun install`; check `packageManager` and the lockfile's name) and commit it alongside `package.json` — CI and hosts install from a frozen lockfile, so a stale one fails the build before it starts. After creating the first new file in the content root (a `meta.ts`, a partial), run `git check-ignore -v <content root>/meta.ts`: a repo `.gitignore` rule that covers the content root leaves every new file out of `git add` while the old pages stay tracked, so fix the rule or report that each new file needs `git add -f`. Blume needs **Node 22.19 or later**: update `.nvmrc`/`.node-version`, `engines.node`, and any CI `node-version` in the same change. pnpm 11 and later run a dependency's build script only once `pnpm-workspace.yaml` approves it: add `allowBuilds: { esbuild: true }` (plus `\"@scarf/scarf\": false`, as `blume init` writes) unless the workspace already lists `esbuild`, keeping its other entries. Add `.blume/`, `dist/`, and `.env.local` to the docs package's own `.gitignore` when it has one, else the root's (as `blume init` does; a `vercel()` build appends `.vercel/` itself), and remove any line that ignores `public/` before moving assets into it. **`blume build` empties `<project>/dist`:** if the host repo already uses `dist/` for something else (Python wheels, a library build), put the Blume project in its own folder (`docs/`). If the repo formats with Prettier, extend its `*.md` overrides (`.prettierrc`, `.editorconfig`) to `*.mdx` and keep `{/* */}` comments on one line. After renaming or moving pages, grep the whole repo (release config, CI, bot and ignore files) for their old paths. Check any `tsconfig.json` in the Blume project dir too: Blume's build reads it, so an `extends` that resolves only after a root workspace install, or `include`s of the old framework's folders, fail `blume build`. Install the workspace first, fix the paths, or delete the file if only the old framework used it (a Nuxt `tsconfig.json` that `extends` or `references` `./.nuxt/*` fails with `BLUME_TSCONFIG_EXTENDS`, and a `postinstall: nuxt prepare` script goes with the Nuxt dependency). **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.71.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.\n7. **Wire up the host repo & deploy (non-trivial repos).** Settings a host keeps in its dashboard (Netlify's publish directory, build command, `NODE_VERSION`) can't be committed: list them in the summary. In `.gitignore`, replace only the docs' own entries and keep ones other packages use. For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`: with `outputDirectory: \"dist\"` for a static build, and with no `outputDirectory` for `deployment: vercel()`, whose build writes `.vercel/output` (add `.vercel/output/**` to Turborepo outputs). Tell the user the settings you can't commit (Vercel Root Directory, Node 22, and server secrets such as `AI_GATEWAY_API_KEY`). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.\n8. **Verify.** Run `blume build` (frontmatter schema, duplicate routes, config — it fails on any error diagnostic by default; **never pass `--no-strict`**, which builds anyway and silently drops pages with invalid front matter or `.mdx` that doesn't parse) and `blume validate --strict` (internal links, heading anchors, assets, redirect targets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).\n\n## The Blume mental model\n\nThe single biggest shift for most sources — especially Mintlify — is that **navigation is derived from the filesystem**, not declared in config.\n\n### Navigation is the file tree\n\n- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.\n- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).\n- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display, directory })`). The `pages` array lists children by slug (numeric prefix, parentheses, and extension stripped; an entry that names nothing warns `BLUME_META_UNKNOWN_PAGE` with the slug it likely meant, so write that one); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order. `directory` inherits down the tree: set `directory: \"none\"` on nested folders that shouldn't list cards. `pages` can't interleave loose pages with subgroups inside a `flat` group, or at the top of the sidebar in any mode: when a source interleaves them (mdBook, MkDocs `navigation.sections`), use `display: \"group\"`, where the order holds inside groups, and say what moved at the top level.\n- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `\"flat\"` (default), `\"group\"` (collapsible), or `\"page\"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).\n- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).\n- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting, **use a `(group)/` folder first**: move the pages into `<folder>/(<group>)/` with a `meta.ts` for the label, order, and `collapsed`. A parenthesized folder adds the sidebar group but no URL segment, so no route changes and no redirects are needed, and a page from another folder can join with a `slug` that keeps its old route. A group whose landing page is a sibling file keeps every URL too: move the landing page to `(group)/index.md` with `slug: <its old route>`, and the slugged index becomes the group's link. Moving a page a folder deeper breaks its relative links and assets (`./img.png`, `../other.md`), so re-base them. (Docsify is the exception: its old `#/` URLs are remapped by script whatever the paths, so its reference moves pages into real folders.) Only when that can't express the shape, move the pages into a real subfolder (URLs change: add `redirects`) or declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.\n\n### Tabs and selectors\n\n- **`navigation.tabs`** (`{ label, path, icon? }`) render top-of-header sections and **scope the sidebar by route** — the folder at a tab's `path` becomes the section, so this needs no config beyond the tabs themselves; structure content as **one folder per tab**. **A source's top-level tabs (Mintlify `navigation.tabs`, a top-level product/section switcher) map to these header tabs — keep them as tabs; don't flatten them into a single global `navigation.sidebar`.** Blume picks the active tab by **URL prefix** (longest tab `path` that prefixes the route), so every page in a tab must live under that tab's single `path`; a source tab that mixes arbitrary routes isn't portable as-is — either move its pages under one prefix (route change → add `redirects`) or accept the closest shape, and say which in the report (details in `references/mintlify.md`). The filtering runs both ways: on a route **under** a tab's `path`, the sidebar shows **only** that tab's folder (a tab also highlights when the current route is under it); on a **root or untabbed** route (or a tab whose `path` is `/`), the tab folders are **hidden** and the sidebar shows only the loose pages that belong to no tab (full tree as a fallback, so it's never blank). Consequence for migrations: once you add tabs, the landing sidebar automatically drops the sectioned content — that's intended, not lost pages; don't hand-build excludes for it.\n- **`navigation.selectors`** (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`, `kind` = `dropdown`/`product`/`version`/`language`) partition a whole site (products, versions) via a header dropdown keyed on the current route.\n- **`navigation.actions`** (`[{ label, href }]`) puts plain links in the **header**, left of the icon buttons, and **`navigation.cta`** (`{ label, href }`, singular) is the header's one filled call-to-action button. This is the home for a source's header bar — a \"Log in\"/\"Status\" link goes in `actions`, a \"Sign up\"/\"Get started\" button in `cta`. An `http(s)` href opens in a new tab; an internal route is validated against your pages at build time, so a route served by another app on the same host must be written as an absolute URL. Both hide on phones (below `sm`), except `cta` on a page with no navigation toggle; a link that must survive on a phone on docs pages belongs in `featured`.\n- **`navigation.featured`** (`{ label, href, icon? }`) pins links to the **top of the sidebar, above every section** — a blog, changelog, or support page that should always be one click away. These are the **exception to tab scoping**: unlike the generated tree, featured links show on **every** route and breakpoint. `href` points anywhere — an external URL opens in a new tab, an internal route (`/contact`) is validated against your pages at build time. `icon` is a Lucide name (or image path/URL/inline SVG), as everywhere else. This is the home for a source's always-visible header/utility links (Mintlify anchors, Blog/Contact links) — see `references/mintlify.md`.\n\n### Routes and pathing\n\n- A route is the content path relative to `content.root`, with **numeric prefixes stripped** (`01-intro.mdx` → `/intro`; a version or a name that starts with a date, `YYYY-MM-DD`, `D-M-YYYY`/`M-D-YYYY`, or `YYYY-MM`, stays whole: `12-05-2022.mdx` → `/12-05-2022`; pin `slug` for any other digit-led name the old site kept whole) and **`(group)/` folders adding no segment**. An `index` file maps to its folder's route. Frontmatter `slug` overrides the generated route. A `README.md` is an ordinary page at `/README`, not its folder's index: rename it `index.md` (Docsify, MkDocs, GitBook, and VuePress sources all use README as the index).\n\n### `blume.config.ts` shape\n\n`defineConfig({...})` — every field optional, all with defaults:\n\n- **Site:** `title`, `description`, `logo` (string SVG, or `{ image: string | { light, dark, alt }, text, href }`), `banner` (a string, or `{ content, link: { href, text }, dismissible, id }` — no color/type). A logo renders beside `title` in the header, so a **wordmark logo doubles the brand** (\"Acme Acme\") — set `text: \"\"` to render the mark alone. **Prefer the string form over `{ light, dark }`:** if you have the logo SVG locally and it's monochrome (solid black or white), rewrite its `fill`/`stroke` to `currentColor` and use `logo: \"/logo.svg\"` — it then inherits the theme's text color and adapts to light/dark automatically, so you don't need separate light/dark files.\n- **`theme`:** `accent` (a color string for both modes, or `{ light, dark }` per mode), `action` (color), `mode` (`light`/`dark`/`system`), `radius`, `fonts` (`{ body, display, mono }` — each a curated Google-font slug, a `{ name, provider?, weights? }` object for any Google/Fontsource/Bunny/Fontshare family, or `{ name, variants: [{ src, weight?, style? }] }` for local font files), `background` and `backgroundImage` (each a string, or `{ light, dark }` per mode). The old `accentDark`/`backgroundDark`/`backgroundImageDark` fields were **merged into these per-mode objects** — a bare string still applies to both modes, so only reach for `{ light, dark }` when the two modes differ. Each color must be a CSS color or an accent preset (a source's bare `\"3b82f6\"` needs its `#`), or `BLUME_THEME_COLOR_INVALID` warns and browsers ignore it. There is **no** `theme.strict` and **no** `theme.css` config field — custom CSS goes in a project-root **`theme.css` file** (auto-picked-up), and a source's \"strict appearance\" flags drop.\n- **`content`:** `root` (default `\"docs\"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `[\"**/*.{md,mdx}\"]` / `[\"**/_*\", \"**/.*\"]` — a set `exclude` **adds to** those defaults, so partials and dot-folders stay unpublished; to publish `_`-prefixed or dot-files on purpose, list the default with a `!`: `exclude: [\"!**/_*\"]`), `sources` (an array of **adapters imported from `blume/sources`**: `filesystem({ root, include, exclude })`, `obsidian({ vault })`, `githubReleases({ owner, repo })`, `notion({ database })`, `sanity({ projectId, dataset, query })`, `contentful({ space, contentType })`, `payload({ url, collection })`, `strapi({ url, contentType })`, `mdxRemote({ github })`, `custom(source)`; every factory with an options object also takes `prefix` and `pollInterval`, while `custom(source)` takes a `ContentSource` instance that sets its own `prefix`. The 1.x `{ type: \"…\" }` objects were removed — rename `type` to the factory call and pass the other fields as its options — except `{ type: \"custom\", source }`, which becomes `custom(source)` with the instance as the only argument. `root`/`include`/`exclude` are shorthand for a single `filesystem()` and are **rejected beside `sources`** — move them into the `filesystem()` entry. OpenAPI/AsyncAPI/GraphQL are **not** among these; they're adapters in the top-level `reference` list), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.\n- **`basePath`** (top-level): a site-wide mount point (e.g. `\"/docs\"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).\n- **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the footer's GitHub link when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see \"Config-declared nesting\" above).\n- **`search`** — an adapter imported from `blume/search`: `orama()` (default, so omit `search` entirely for it), `flexsearch()`, `pagefind()`, `algolia({ appId, apiKey, indexName })`, `oramaCloud({ endpoint, apiKey, indexId? })`, `typesense({ host, collection, apiKey })`, `mixedbread({ storeId })`, or `false` to disable. Pass the adapter directly (`search: algolia({…})`) or as `search: { provider, popular, indexing }` when you also set curated links or indexing options. **Blume 1.x's `search.provider` string and its `search.algolia`/`oramaCloud`/`typesense`/`mixedbread` credential blocks are gone** — when a source config (or an older `blume.config.ts`) has `provider: \"algolia\", algolia: { appId, indexName, searchApiKey }`, rewrite it as `search: algolia({ appId, indexName, apiKey: searchApiKey })` (the search-only key is now `apiKey` in every adapter that takes one; admin keys stay in env vars). **`ai`** (the assistant, Open in chat — `ai.assistant.provider` takes an **adapter descriptor** imported from `blume/ai`: `gateway({ model })` (the default, `openai/gpt-5.5`), `openai({ model })` (or `openai({ baseUrl, name, model, apiKeyEnv })` for any OpenAI-compatible endpoint), `anthropic({ model })`, `gemini({ model })`, `grok({ model })`, `openrouter({ model, reasoning })`, `llmgateway({ model })`, or `inkeep({ model })`. Every adapter takes `model`, `apiKeyEnv`, `headers`, and a verbatim `providerOptions` passthrough (`headers` values are written into the generated route source as-is, so they are for non-secret static headers only — a bearer token or any other credential belongs in the env var `apiKeyEnv` names, never in `headers`). Every adapter except `inkeep()` also takes `reasoning`: Inkeep runs its own answer pipeline, so `inkeep({ reasoning })` fails validation with an unrecognized key — drop a source's reasoning setting there and report it. There are **no** flat `provider`/`model`/`apiKeyEnv`/`baseUrl`/`headers`/`reasoning` fields on `ai.assistant` — a source that configured an AI assistant that way (Blume < 2.0 included) maps onto one adapter call. `enabled`, `instructions`, `retrieval`, `suggestions`, `cors`, and `endpoint` sit on `ai.assistant` itself; there is **no** `ai.ask` — Blume 2.0.0 and earlier used that name, so an older `blume.config.ts` moves the whole block to `ai.assistant`), **`agents`** (llms.txt, the JSON API, the MCP server, published skills, discovery manifests, robots content signals), **`reference`** (a list of adapters imported from `blume/reference` — `openapi({ spec | sources, route, … })`, `asyncapi({ … })`, `graphql({ spec, endpoint, … })` — never the 1.x `openapi`/`asyncapi`/`graphql` blocks), **`redirects`**, **`seo`**, **`markdown`**, **`analytics`** (a list of adapters imported from `blume/analytics` — one factory per provider (`posthog({ key, host })`, `googleAnalytics({ id })`, `plausible({ domain, host })`, `mixpanel({ token, region })`, `segment({ key })`, …; the docs page lists them all), `vercel()`, and `script({ src | content, strategy, attributes })` for anything without one — never an object keyed by provider), **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).\n- **`analytics`** — adapters from `blume/analytics`, including `script()` for a source's custom head scripts. A known analytics snippet in the source's head scripts (the gtag loader plus `gtag('config')` pair, GTM, Plausible, …) becomes that provider's factory, not `script()`. Adapters load **only in production builds and wait for `consent`** when it's configured, so a script a page needs to work (a support widget, a form embed) goes in a `PageFooter` layout slot component instead (`defineComponents`).\n- **`deployment`** is a host adapter from `blume/deploy` — `vercel()`, `netlify()`, `cloudflare()`, or `node()` — or a plain `{ site, base }` for a static build on any host (the default is a static build). Naming a host adapter switches the build to **server output** on that host (pass `output: \"static\"` to stay static there); `site`, `base`, and `output` are the named options, and anything else is forwarded verbatim to the underlying `@astrojs/*` adapter. There is **no** `deployment.adapter` string and **no** `deployment.output` field: a source that needs server rendering (the assistant, the MCP server, Mixedbread search, the API playground proxy) gets `import { vercel } from \"blume/deploy\"` and `deployment: vercel()`; a static source gets nothing — unless it served its docs under a subpath, which becomes `deployment: { base: \"/docs\" }` (see below for when to add `site`). Note that `cloudflare` and `vercel` are also exported from `blume/analytics` — alias one (`import { cloudflare as cloudflareDeploy } from \"blume/deploy\"`) when a config uses both.\n- **Keep the source's site URL as `deployment.site` unless the target host is one Blume auto-detects.** Blume fills `site` in from the platform's build environment only on **Vercel and Netlify** (their stable production domain), and uses the dev server's `localhost` URL during `blume dev`; on those two hosts leave it unset and let detection pick the deployed URL. Everywhere else — Cloudflare Pages (it exposes only the per-deploy `CF_PAGES_URL`), GitHub Pages, S3 or another static host, a custom CDN, Cloudflare Workers, a `node()` server — nothing detects it, and an unset `site` silently drops the sitemap, OG images, RSS feeds, the AI catalog, and absolute canonical URLs. So when the source config had a `url`/`site` field and the target isn't one of those two hosts, carry it over: `deployment: { site: \"https://…\" }` for a static build, or the `site` option of a host adapter (`node({ site })`). If you can't tell where the site will deploy, keep it and say so in the report.\n- **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png` or `apple-touch-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.\n\nThe schema is exported from `blume/schema`; the full field reference is in the `docs/configuration/` directory of the installed `blume` package (see \"Full documentation\" below for how to locate it).\n\n### Icons are Lucide, period\n\nBlume resolves **bare kebab-case [Lucide](https://lucide.dev) names** everywhere an icon is accepted — frontmatter `icon`, `sidebar.icon`, `meta.ts` `icon`, `navigation.tabs`/`selectors` icons, and `Card`/`Step`/`Icon`/etc. props. There is **no** FontAwesome or Tabler support and **no** `iconType` prop. Names must be **kebab-case** (`book-open`, not `BookOpen`) — a PascalCase React-component name (common in Fumadocs/lucide-react sources) does not resolve and renders nothing. When migrating a source that uses another icon set (Mintlify and Fern use FontAwesome), **map each name to its closest Lucide equivalent**; where none exists, drop the icon and report it. Verify a name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it.\n\n### Page frontmatter (strict — unknown keys are build errors)\n\n```yaml\n---\ntitle: Install # renders as the page H1 — remove any duplicate H1 in the body\ndescription: Install Blume and scaffold your first project.\ntype: doc # doc (default); blog and changelog drive feeds, other values only mean something under content.types\nsidebar:\n  label: Install # overrides title in the sidebar\n  order: 2\n  icon: download\n  badge: New\n  hidden: false\nseo:\n  title: …\n  description: …\n  image: /og/install.png\n  canonical: https://…\n  noindex: false\nsearch:\n  exclude: false\n  tags: [api]\nslug: install # override the generated route\ndraft: false\nlastModified: 2026-06-20 # pin the \"last updated\" date\n---\n```\n\nAlso valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog metadata), `deprecated`, and the top-level shorthands `hidden` (for `sidebar.hidden`) and `noindex` (for `seo.noindex`).\n\n### Authoring features (no imports needed in `.mdx`)\n\n- **The rich features are MDX-only.** Directives, `package-install`, mermaid, and math are wired into the MDX processor; in a plain `.md` file a `:::note` stays **literal text** — and the build stays green, with only a `BLUME_MD_DIRECTIVE` warning at its line. **Rename any `.md` file that uses (or should use) these to `.mdx` during migration.** MDX is stricter prose than Markdown: any `<` followed by a non-space character that doesn't open a tag (`<7`, `<50 ms`, `<= 5`, `List<T>`) and a bare `{` or `}` fail the build, so escape them (`&lt;` or `\\<`, and `\\{`) in any page you rename. With `lastModified: \"git\"`, every page the migration edits shows the migration commit's date, so say so in the report when the site shows dates; a page it only renames or moves keeps its date (Blume follows renames). This bites hardest on Docusaurus/Starlight sources, whose `.md` content is full of `:::` admonitions. Plain Markdown (headings, tables, fenced code with titles/highlighting) is fine in `.md`.\n- **Callouts as directives:** `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::info`, `:::success`, with an optional title in brackets, which renders its inline Markdown: `:::warning[Heads up]`. Aliases `caution`→warning, `error`→danger, `important`→note, `warn`→warning. A callout that needs its own icon or tint is `<Callout type icon color title>` (a Lucide `icon`, any CSS `color`), which a directive can't set. **markdown-it sources (VitePress, VuePress, Docusaurus v2) write `::: tip Title`: with a space after the colons it's never a directive, even in `.mdx`, and renders as literal text (`.mdx` warns `BLUME_DIRECTIVE_SPACED_NAME` only for a callout name); inside an open `:::` container it closes that container and its text is dropped (`BLUME_DIRECTIVE_CLOSING_TEXT`); a title after the name (`:::tip Title`) is dropped too (`BLUME_DIRECTIVE_OPENING_TEXT`).** Normalize to `:::tip[Title]`. MDC's two-colon blocks (Docus, any Nuxt Content site: `::note` … `::`) are leaf directives to Blume and print as text (`BLUME_MDC_SYNTAX`), and MDC `[text]{.class}` spans fail the MDX build (`BLUME_MDX_ATTRIBUTE_LIST` names the line). GitHub alerts (`> [!NOTE]`, any of GitHub's five, `[!CAUTION]` as danger) render as callouts in `.mdx`, text after the marker as the title; in `.md` they stay quotes that show the marker (`BLUME_MD_GITHUB_ALERT`), so rename those pages to `.mdx`.\n- **No-import MDX components:** `Callout`, `Card`/`CardGroup`, `Columns`/`Column`, `Steps`/`Step`, `Tabs`/`Tab`, `Accordion`/`AccordionItem`, `Expandable`, `FileTree`, `Tree`/`Tree.Folder`/`Tree.File`, `CodeGroup`, `Frame`, `Panel`, `Tooltip`, `Tile`, `Badge`, `Icon`, `TypeTable`/`AutoTypeTable`, `Color`, `YouTube`, `Visibility`, `GithubInfo`, `Component`, `CodeBlock`, `Diff`, `Prompt`, `Math`, `Changelog`, and the hand-written endpoint fields `ParamField`/`ResponseField`/`RequestExample`/`ResponseExample` (Mintlify's semantics: `<ParamField path|query|header|body=\"name\">` names the field by its location; see `docs/references/api-pages.mdx`). (**Not** shipped — convert away: `<Warning>`/`<Note>`/`<Info>`/`<Tip>`/`<Check>`/`<Error>` → the matching `:::` directive, and `RequestField`. A `<CodeBlock>` with no `code` prop, used as a wrapper around a fence as Fern and Mintlify do, renders the fence as written but ignores its own `title`: unwrap the fence and put the title in its meta. A component that shows its children, used with none and another tool's props (`<Badge type=\"tip\" text=\"beta\" />`), renders empty and warns `BLUME_UNKNOWN_PROP`: put the content between the tags. See the reference files for targets.)\n- **Fenced-code superpowers:** ` ```package-install ` → package-manager tabs; ` ```mermaid ` → a rendered diagram; code-block titles (` ```ts server.ts `), line numbers (`lineNumbers`), and highlighting (`{1,4-5}`, `// [!code ++]`).\n- **Fences and titles that trip migrations:** a fence language Shiki doesn't know renders as plain text and warns `BLUME_UNKNOWN_CODE_LANGUAGE` (case doesn't matter: `JSON` highlights like `json`): fix the id, or write `text` for plain text. A language glued to attributes with a comma (` ```rust,ignore `) is one of those; with a space instead, rustdoc's and mdBook's keywords (`ignore`, `no_run`, `should_panic`, `edition2021`, `editable`, …) warn `BLUME_CODE_FENCE_OPTION` and stay out of the title, but any other second word becomes the block's title: keep only the language. Other tools' fence options (`hl_lines=\"2 3\"`, `linenums=\"1\"`, `showLineNumbers`, Mintlify's `lines`, `wordWrap`, `filename=\"…\"`) do nothing, stay out of the title, and warn `BLUME_CODE_FENCE_OPTION` with the Blume spelling (`{2,3}`, `lineNumbers`, `wrap`, `title=\"…\"`). A fence with options but no language needs one (` ```text title=\"x\" lineNumbers `), or the first option is read as the language. A title is every word after the language, and a bracketed one (` ```ts [file.ts] `, from VitePress and MDC) drops its brackets, with a range glued after them (`[file.ts]{2}`) highlighting its lines. `Expandable`, `AccordionItem`, and callout titles render inline Markdown (code, emphasis, links); a `Step` title is a plain string that shows Markdown as written and has no anchor, so keep an anchored heading as the first line inside `<Step>`. Raw HTML `<img src>` and `<a href>` are never optimized: a relative one naming a file beside the page publishes that file with the page (as `[spec](./spec.pdf)` and `<Card img=\"./cover.png\">` do), a root path gains `deployment.base` like a Markdown link, and `blume validate` checks both, but a relative `<a href>` to a page resolves against the page's URL, not its folder. Convert images to Markdown so they're optimized, and page links to Markdown links or root routes.\n- **Math:** `$$…$$` renders in `.mdx` with **no conf\n\n<!-- [TRUNCATED AT 50KB] -->\n\n<!-- File: .agents/skills/blume-update-docs/references/audit-checklist.md (Tokens: ~645 | Category: anthropic-skill) -->\n# Audit checklist\n\nUse this checklist to decide whether a docs-maintenance run should make changes.\n\n## Sources to check\n\n- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.\n- **Blume config:** `blume.config.ts` — `content.root`, navigation (`meta.ts` files, tabs, selectors), site settings, AI/MCP settings, search, OpenAPI/AsyncAPI sources, theme, and export settings.\n- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.\n- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.\n- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.\n- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.\n- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.\n- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.\n\n## Change criteria\n\nMake a docs edit when at least one condition is true:\n\n- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.\n- A documented workflow no longer works or misses a required step.\n- A page promises a feature, provider, adapter, or integration the code no longer supports.\n- A new user-facing capability shipped but is absent from the appropriate docs page.\n- A link points at moved, removed, or outdated primary documentation.\n- A changelog or release page needs an entry for shipped user-facing behavior.\n\nSkip the edit when the only available change is subjective polish, wording preference, duplicated information, speculative future work, or behavior still hidden behind a feature flag.\n\n## Blume editing guidance\n\n- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.\n- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.\n- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.\n- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.\n- Keep internal links root-relative (`/guides/setup`), in the form nearby pages already use.\n- Do not edit generated `.blume/` or `dist/` output.\n\n## PR notes\n\nInclude these sections in the PR body or no-op summary:\n\n- Sources checked\n- Docs changed\n- Verification run (commands and results)\n- Skipped checks, with reasons\n- Remaining risk or follow-up\n\n<!-- File: .agents/skills/blume-update-docs/SKILL.md (Tokens: ~1122 | Category: anthropic-skill) -->\n---\nname: blume-update-docs\ndescription: Keep a Blume docs site in sync with the product it documents. Audit recently merged pull requests, changelogs, config schemas, CLI help, and public APIs against the docs content, update only pages that are factually stale, verify the docs build, and open (or update) a maintenance pull request — or report a clean no-op. Use when asked to check docs for drift, refresh stale documentation, run a scheduled docs audit, or keep docs current after a release.\n---\n\n# Update Blume Docs\n\nBlume is a **markdown-first** documentation framework on Astro/Vite: content lives as Markdown/MDX under a content root (default `docs/`), navigation derives from the file tree plus optional `meta.ts` files, `blume build` validates frontmatter and duplicate routes, and `blume validate` checks links and anchors.\n\nYour job is **docs maintenance, not docs authorship**: find where shipped, user-facing behavior has drifted from what the docs claim, fix exactly that, prove the site still builds, and deliver the result as a pull request. A run that finds nothing actionable ends with a short report and **no branch, no commit, no PR** — prefer a no-op over a noisy PR.\n\n## Ground rules\n\n- **Only document what shipped.** Never invent features, timelines, pricing, APIs, or compatibility claims. Work behind a feature flag is not ready for docs unless the flag is enabled for the documented audience or the repo explicitly documents unreleased behavior.\n- **Facts over polish.** Edit when a command, option, default, route, prop, or workflow is wrong or missing. Skip subjective rewording, marketing polish, restructuring, and formatting-only churn.\n- **Smallest correct diff.** Touch the fewest pages that remove the drift. Preserve the site's voice, frontmatter style, component usage, and `meta.ts` navigation patterns.\n- **Exact source-of-truth wording** for commands, flags, config keys, environment variables, routes, and version numbers — copy them from code, don't paraphrase from memory.\n- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.\n\n## Workflow\n\n1. **Establish context.**\n   - Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.\n   - Locate the docs app and content root: `blume.config.ts` (`content.root`), the directory of `.md`/`.mdx` pages, `meta.ts` files, and the package manager + docs build command.\n   - If this run was configured with a trigger, lookback window, docs path, target branch, or PR policy, honor those. Use the defaults below only where the prompt is silent.\n\n2. **Reuse or create a maintenance branch.**\n   - If an open docs-maintenance PR from a previous run exists (head branch starting with `blume/`), check out and update that branch instead of opening a duplicate.\n   - Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.\n\n3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:\n   - Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.\n   - Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.\n   - Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.\n   - Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.\n\n4. **Update the docs.**\n   - Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.\n   - Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.\n\n5. **Verify.**\n   - Run the docs build (`blume build` or the repo's documented docs QA) — it validates frontmatter and duplicate routes.\n   - Run `blume validate` to check internal links and anchors.\n   - Run lint/format/typecheck when the repo's conventions call for them on docs changes.\n   - Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.\n\n6. **Deliver.**\n   - **Changes made:** commit only the maintenance edits, push the `blume/*` branch, and open or update a PR against the default branch titled like `blume: refresh docs for YYYY-MM-DD`. In the body list sources checked, docs changed, verification commands and results, skipped checks, and residual risk.\n   - **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.\n\n## Resources\n\n- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.\n\n<!-- File: .agents/skills/blume/SKILL.md (Tokens: ~1924 | Category: anthropic-skill) -->\n---\nname: blume\ndescription: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files.\n---\n\n# Blume\n\nBlume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain.\n\nThe core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control.\n\n## What makes it different\n\n- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them.\n- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, publishes a JSON docs API (`/api/docs/…`) described by an OpenAPI document at `/openapi.json`, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional in-page **assistant** or an **MCP server** so coding agents read your docs directly.\n- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens.\n- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build.\n\n## Quickstart\n\nBlume needs **Node.js 22.19 or newer**. From an empty folder:\n\n```bash\nnpx blume init   # scaffold docs/index.mdx, blume.config.ts, and package.json scripts, then install\nnpm run dev      # dev server with hot reload\nnpm run build    # static HTML to dist/, with a local search index\n```\n\nIn a project that already has a `package.json`, `blume init` leaves it alone: add `\"dev\": \"blume dev\"` and `\"build\": \"blume build\"` to its scripts, or run `npx blume dev`. Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.\n\n### Writing a page\n\nEvery page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**.\n\n```mdx\n---\ntitle: Introduction\ndescription: Welcome to my docs.\n---\n\nWelcome! Use **Markdown** and built-in components — no imports required:\n\n:::note\nBlume ships callouts, cards, tabs, steps, and more.\n:::\n```\n\nNavigation, search, and page metadata are inferred from your files as you add them.\n\n## Upgrading from Blume 1\n\nBlume 2 changes configuration, not content: search, deployment, content sources, API references, analytics, and the assistant's model backend become adapters imported from `blume/*` subpaths (`search: algolia({ … })` from `blume/search`), Ask AI is renamed the assistant (`ai.ask` becomes `ai.assistant`), the machine-readable settings move from `ai` to `agents`, and `components.ts` entries must be static. From the folder with `blume.config.ts`, run:\n\n```bash\nnpx blume@latest upgrade\n```\n\nIt bumps `blume` in `package.json`, installs, and lists every config change still needed with its file, line, and replacement (plus `package.json` scripts that pass removed `blume build` flags, and pages whose frontmatter sets a removed field), exiting non-zero until none are left — rerun it after each round of fixes. (`--codex` or `--claude` hands that list to an agent CLI from a terminal.) When you are the agent doing the upgrade, work from that list and the upgrade guide, `docs/03-upgrading.mdx` in the installed package, which has before-and-after examples for every change. Keep the site's behavior the same, and verify with `blume doctor` and `blume build`.\n\n## Migrating from another framework\n\nTo move a Mintlify, Docusaurus, GitBook, MkDocs, ReadMe, or other docs site to Blume, the user runs `npx blume migrate [source] --codex` (or `--claude`) from that project, which opens an agent on the `blume-migrate` skill. When you are that agent, or the user asks you to migrate directly, follow `skills/blume-migrate/SKILL.md` in the installed package instead of this file.\n\nEvery build publishes a generated agent skill for the site at `/skill.md` (`agents.skillMd`). To write a richer one from the docs, the user runs `blume skill --codex` (or `--claude`), which opens an agent on the `blume-write-skill` skill. When you are that agent, or the user asks you to write the site's skill directly, follow `skills/blume-write-skill/SKILL.md` in the installed package.\n\n## What's included\n\n- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.\n- **Local search** — Orama in dev and production, with no hosted index; Pagefind, Algolia, and other backends are one adapter away (`search: pagefind()` from `blume/search`).\n- **AI** — `llms.txt`, raw Markdown URLs, a JSON docs API with an OpenAPI description, Copy as Markdown, Open in chat, an in-page assistant, and an MCP server endpoint served by the docs site itself.\n- **Narration** — a \"Listen to this page\" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build; `openai({ model, voice })` uses OpenAI's speech models instead, and with a `baseUrl` a self-hosted OpenAI-compatible speech server, and needs `@ai-sdk/openai` installed).\n- **Navigation** — inferred from files, refined with `meta.ts` or config.\n- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.\n- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.\n- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.\n\n## How it works\n\nThe Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it.\n\n## Full documentation\n\nThis is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the `docs/` directory bundled inside the installed `blume` package.\n\n**Locate the package first — it is not always at the repository root.** In a workspace monorepo (pnpm especially), `blume` is installed in the depending workspace's `node_modules` (e.g. `apps/docs/node_modules/blume/docs`), not the root. From the package that depends on `blume`, this prints the exact location:\n\n```bash\nnode -e \"console.log(require.resolve('blume/package.json'))\"\n```\n\nThe docs sit in `docs/` next to that `package.json`. Start with `docs/index.mdx` (Introduction) and `docs/01-quickstart.mdx`, then list the `docs/` directory: each section is a folder — configuration, content authoring, API references, discoverability (SEO and the agent-facing surface), the CLI, and advanced topics — so open the one that covers the task.\n\n<!-- File: skills/orpc-contract/SKILL.md (Tokens: ~2588 | Category: anthropic-skill) -->\n---\nname: orpc-contract\ndescription: \"Design oRPC v2 APIs contract-first, defining the API shape with oc from `@orpc/contract`, implementing it with implement from `@orpc/server`, and consuming the contract from typesafe clients. Use when a project depends on `@orpc/contract`, when defining a contract with oc, implementing a contract with implement, sharing an API contract between server and client packages, generating a contract from an existing OpenAPI spec, or publishing a typed API client to npm. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For core builder, serving, and client work without a contract, use the orpc skill; for REST/OpenAPI exposure, spec generation, and OpenAPILink details, use the orpc-openapi skill.\"\nlicense: MIT\n---\n\n# oRPC Contract-First\n\nContract-first oRPC splits an API into two artifacts: a contract (schemas, errors, metadata, no business logic) defined with `oc` from `@orpc/contract`, and an implementation built from it with `implement` from `@orpc/server`. Server and client both depend on the contract, never on each other, so the API shape can live in its own package, be reviewed on its own, and ship to consumers as a typed SDK. Prefer it when server and client are separate packages or teams, when the shape starts from an existing OpenAPI spec, or when publishing a client to npm. Stay with the `os`-first flow when one codebase holds both sides and the client can import the router type directly; converting later is cheap because a plain router already works as a router contract (resolve lazy routers with `unlazyRouter` first).\n\nThis skill targets oRPC v2. The `orpc` skill carries the v2 install and version check plus core builder, middleware, serving, and client concepts. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its docs page first (see [Full documentation](#full-documentation)).\n\n## Define the contract with oc\n\nEvery chain is optional and each call returns a new instance, so share base contracts freely. A contract has no `.handler`. Zod, Valibot, ArkType, and any other Standard Schema library work.\n\n```ts\nimport { oc } from '@orpc/contract'\nimport * as z from 'zod'\n\nexport const contract = {\n  planet: {\n    list: oc\n      .output(z.array(z.object({ id: z.number(), name: z.string() }))),\n    find: oc\n      .errors({ NOT_FOUND: {} })\n      .input(z.object({ id: z.number() }))\n      .output(z.object({ id: z.number(), name: z.string() })),\n  },\n}\n```\n\n- Always define `.output`: without it clients infer the output as `unknown`.\n- A router contract is a plain object mapping keys to procedure contracts or nested objects. Avoid the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n- Attach shared metadata to a whole subtree with `oc.meta(someMeta).router({...})`.\n- `.errors({ NOT_FOUND: {} })` declares typesafe errors; implementations throw them via `errors.NOT_FOUND()` and clients infer their shapes.\n- Repeated `.input`/`.output` calls stack schemas instead of replacing them: object input schemas compose into one flat value, output schemas pipe. Use this to extend a base contract without repeating fields.\n- Schema-library-free contracts use the `type` utility from `@orpc/contract`: `oc.input(type<{ value: number }>())`, optionally with a mapping function as `type<Input, Output>(fn)`.\n- REST routes attach via `.meta(openapi({ method: 'GET', path: '/planets/{id}' }))` from `@orpc/openapi`, exactly as on `os`; routing rules, `prefix`, and spec generation belong to the `orpc-openapi` skill.\n\nInfer types with `InferRouterContractInputs`, `InferRouterContractOutputs`, and `InferRouterContractErrors` from `@orpc/contract`.\n\n## Implement with implement\n\n`implement` turns the contract into an implementer that mirrors its shape and type-checks every handler; `.router` also enforces the contract at runtime.\n\n```ts\nimport { implement } from '@orpc/server'\n\nconst implementer = implement(contract).$context<{ db: DB }>()\n\nconst listPlanets = implementer.planet.list.handler(async ({ context }) => context.db.list())\n\nconst findPlanet = implementer.planet.find.handler(async ({ input, context, errors }) => {\n  const planet = await context.db.find(input.id)\n  if (!planet)\n    throw errors.NOT_FOUND()\n  return planet\n})\n\nexport const router = implementer.router({\n  planet: { list: listPlanets, find: findPlanet },\n})\n```\n\n- `.$context` declares the initial context the procedures require, as on `os`.\n- Apply middleware per procedure with `.use(mw)` before `.handler`. That runs after input validation (the contract already registered `.input`); to wrap validation, apply it at router level: `implementer.use(mw)` for every procedure, or `implementer.planet.use(mw).list` for a subtree. Router-level plus procedure-level `.use` can run the same middleware twice; use the dedupe pattern from the `orpc` skill.\n- `implementer.middleware(fn)` creates middleware that infers the contract's typesafe errors. When not every procedure defines a code, guard with the `in` operator: `if ('TOO_MANY_REQUESTS' in errors) throw errors.TOO_MANY_REQUESTS()`. Any type-compatible middleware also works.\n- The result is a normal router: serve it with `RPCHandler` (`orpc` skill) or `OpenAPIHandler` (`orpc-openapi` skill), call it in-process with `call` or `createRouterClient` from `@orpc/server`.\n\n## Consume the contract from clients\n\n`RPCLink` needs only the contract type; `OpenAPILink` takes the contract as a runtime value to read each procedure's route. Get the client types exactly right:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\n// RPC protocol (server side is RPCHandler)\nconst rpcLink = new RPCLink({ origin: 'https://api.example.com', url: '/rpc' })\nconst client: RouterContractClient<typeof contract> = createORPCClient(rpcLink)\n\n// OpenAPI protocol (OpenAPIHandler or any spec-compliant server)\nconst openapiLink = new OpenAPILink(contract, { origin: 'https://api.example.com', url: '/api' })\nconst apiClient: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(openapiLink)\n```\n\n- Router-first equivalents use `RouterClient<typeof router>` from `@orpc/server` in the same positions.\n- `JsonifiedClient` is required over `OpenAPILink` because OpenAPI serialization is one-way (a `Date` returns as a string); dropping it via Smart Coercion, plus `OpenAPILink` options and CORS caveats, are in the `orpc-openapi` skill.\n- Per-call client context is the second type parameter, `RouterContractClient<typeof contract, ClientContext>`, then `client.planet.find(input, { context: { token } })`; link options like `headers` accept functions of that context.\n- Export `RouterContractClient<typeof contract>` as a type from the server package so clients never import the contract module itself (still needed as a runtime value for `OpenAPILink`; ship the minified JSON below).\n- In very large codebases, skip the root client: pin each procedure with `.meta(meta.path([...]))` and build per-procedure clients with `createContractClientFactory` from `@orpc/contract` (`createContractJsonifiedClientFactory` from `@orpc/openapi` when the link needs `JsonifiedClient`); fetch `contract/client-factory` before adopting it.\n\n## Ship the contract\n\nWhen the contract is derived from a router, importing it on the client is heavy and may expose internals. Minify and export JSON instead:\n\n```ts\nimport fs from 'node:fs'\nimport { minifyRouterContract } from '@orpc/contract'\nimport { unlazyRouter } from '@orpc/server'\n\nconst minified = minifyRouterContract(await unlazyRouter(router))\nfs.writeFileSync('./contract.json', JSON.stringify(minified))\n```\n\n`minifyRouterContract` keeps only client-needed metadata. On the client, import the JSON and cast, since schemas do not survive serialization: `new OpenAPILink(contract as typeof router, ...)`.\n\nTo publish a typed SDK to npm, export a factory that pairs the contract with a link:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nexport function createMyApi(apiKey: string): RouterContractClient<typeof contract> {\n  const link = new RPCLink({\n    origin: 'https://example.com',\n    url: '/rpc',\n    headers: { 'x-api-key': apiKey },\n  })\n  return createORPCClient(link)\n}\n```\n\nBundle with `tsdown --dts src/index.ts`, point `exports` at `dist` types plus import entries, list `@orpc/client` and `@orpc/contract` as dependencies, and publish. Consumers get a fully typed client that works with every oRPC client integration (TanStack Query included). Fetch `recipes/publish-client-to-npm` for the complete `package.json`.\n\n## Generate the contract from an existing OpenAPI spec\n\nUse Hey API's `orpc` plugin instead of hand-writing the contract. Install `@hey-api/openapi-ts@next` as a dev dependency (oRPC v2 output requires the `next` tag until the next stable Hey API release) and create `openapi-ts.config.ts`:\n\n```ts\nimport { defineConfig } from '@hey-api/openapi-ts'\n\nexport default defineConfig({\n  input: 'https://example.com/openapi.json', // local file or URL\n  output: 'src/contract',\n  plugins: [{ name: 'orpc', compatibilityVersion: '2', validator: 'zod' }],\n})\n```\n\nThen run `npx @hey-api/openapi-ts`. It writes `orpc.gen.ts` (one procedure contract per operation, routed via `.meta(openapi({...}))` with `inputStructure: 'detailed'`, plus a combined `contract` router) and `zod.gen.ts`. The generated files import `@orpc/contract`, `@orpc/openapi`, and `zod`, so install those too. From there, implement the contract on your own server, or point `OpenAPILink` at the existing spec-compliant server.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/contract/procedure.md)\n\nPages to fetch when you need details beyond this skill:\n\n- Contract: `contract/procedure`, `contract/router`, `contract/implementation`, `contract/generate-from-openapi`\n- Clients: `client/client-side`, `client/server-side`, `client/error-handling`, `openapi/link`\n- Workflows: `recipes/publish-client-to-npm`, `contract/client-factory`, `recipes/monorepo-setup`\n\n<!-- File: skills/orpc-migrate/SKILL.md (Tokens: ~2595 | Category: anthropic-skill) -->\n---\nname: orpc-migrate\ndescription: \"Migrate existing codebases to current oRPC, covering tRPC to oRPC (incremental wrapping via the @orpc/trpc integration or a full rewrite with the concept mapping) and oRPC v1 to v2 (package renames, breaking changes, and a safe order of operations). Use when asked to migrate from tRPC to oRPC, convert or wrap a tRPC router, upgrade oRPC v1 to v2, fix oRPC v2 breaking changes, or swap `@trpc/*` packages for `@orpc/*` equivalents. Biases toward retrieval from the oRPC docs over pre-trained knowledge. Not for greenfield oRPC work or new features in an already-migrated codebase: use the orpc skill for those.\"\nlicense: MIT\n---\n\n# Migrating to oRPC\n\nPlaybook for two migrations: tRPC to oRPC, and oRPC v1 to v2. Work in small mechanical steps and run the project's typecheck and test suite after each one, so any failure points at the last step. Pretrained knowledge of oRPC describes v1 and is often wrong for v2: derive every import path, builder method, and option name from the docs pages listed at the end, never from memory. For core v2 concepts while rewriting (the `os` builder, routers, middleware, clients), load the `orpc` skill. v2 currently ships under the `beta` npm dist-tag and plain installs get v1; drop the `@beta` suffix once `npm view @orpc/server dist-tags` shows `latest` at 2.x.\n\n## tRPC to oRPC\n\nTwo paths. Pick incremental when the app must keep shipping or the tRPC router is large; pick the full rewrite when the router is small enough to convert in one pass.\n\n### Incremental: wrap the existing tRPC router\n\nInstall `@orpc/trpc@beta` and convert. The result is a regular oRPC router: expose it through an RPC or OpenAPI handler, or call it with a server-side client, while the tRPC code keeps working untouched.\n\n```ts\nimport { toORPCRouter } from '@orpc/trpc'\n\nconst orpcRouter = toORPCRouter(trpcRouter)\n```\n\n- tRPC error formatting is not supported: tRPC errors arrive wrapped in `ORPCError` with the `TRPCError` as `cause` (and a `ZodError` below that for validation failures). Reshape them in a handler interceptor if consumers need structured errors.\n- `toTRPCMeta` bridges oRPC `openapi()` metadata into tRPC `.meta()` so converted procedures get OpenAPI routing. Chained tRPC `.meta()` calls merge shallowly, so keep all oRPC metadata inside a single `toTRPCMeta` call.\n\nThen rewrite leaf routers to native oRPC one at a time, mounting each next to the converted router in one plain object.\n\n### Full rewrite: concept map\n\n| Concept           | tRPC                                           | oRPC                           |\n| ----------------- | ---------------------------------------------- | ------------------------------ |\n| Router            | `t.router({...})`                              | plain object                   |\n| Procedure builder | `t.procedure`                                  | `os`                           |\n| Context           | `initTRPC.context<T>()`                        | `os.$context<T>()`             |\n| Create middleware | `t.middleware(fn)`                             | `os.middleware(fn)`            |\n| Use middleware    | `.use(mw)`                                     | `.use(mw)`                     |\n| Validation        | `.input(schema)` / `.output(schema)`           | same names                     |\n| Implementation    | `.query()` / `.mutation()` / `.subscription()` | `.handler()` for all three     |\n| Errors            | `new TRPCError({ code, ... })`                 | `new ORPCError(code, { ... })` |\n| Serializer        | `superjson` transformer                        | built in, remove `superjson`   |\n\nSteps, in order, verifying after each:\n\n1. **Packages.** Remove `@trpc/server`, `@trpc/client`, `@trpc/tanstack-react-query`; install `@orpc/server@beta`, `@orpc/client@beta`, `@orpc/tanstack-query@beta`.\n2. **Base file.** Port the context factory unchanged, then rebuild the shared procedures. In handlers and middleware, `ctx` becomes `context`:\n\n   ```ts\n   import { ORPCError, os } from '@orpc/server'\n\n   const o = os.$context<Awaited<ReturnType<typeof createContext>>>()\n\n   export const publicProcedure = o.use(timingMiddleware)\n   export const protectedProcedure = publicProcedure.use(({ context, next }) => {\n     if (!context.session?.user)\n       throw new ORPCError('UNAUTHORIZED')\n     return next({ context: { session: context.session } })\n   })\n   ```\n\n3. **Procedures.** Replace `.query`/`.mutation`/`.subscription` with `.handler`; `.input` and `.output` carry over as is.\n4. **App router.** Delete every `createTRPCRouter()` wrapper; nested plain objects are the router.\n5. **Server.** Replace the tRPC adapter with an oRPC handler for the runtime (fetch shown; other adapters exist for Node, Fastify, AWS Lambda, WebSocket):\n\n   ```ts\n   import { RPCHandler } from '@orpc/server/fetch'\n\n   const handler = new RPCHandler(appRouter)\n\n   const { response } = await handler.handle(request, {\n     prefix: '/api/orpc',\n     context: await createContext({ headers: request.headers }),\n   })\n   ```\n\n6. **Client.** `RPCLink` plus `createORPCClient`, typed by `RouterClient`. Call sites drop the `.query()`/`.mutate()` suffixes:\n\n   ```ts\n   import type { RouterClient } from '@orpc/server'\n   import { createORPCClient } from '@orpc/client'\n   import { RPCLink } from '@orpc/client/fetch'\n\n   const link = new RPCLink({ origin: 'http://localhost:3000', url: '/api/orpc' })\n   export const client: RouterClient<typeof appRouter> = createORPCClient(link)\n\n   const { planets } = await client.planet.list({ cursor: 0 })\n   ```\n\n7. **TanStack Query.** `createTanstackQueryUtils(client)` replaces the provider and `useTRPC` hook entirely; use the utils object directly. Input moves inside an `input` key: `orpc.planet.list.queryOptions({ input: { cursor: 0 } })`, `orpc.planet.create.mutationOptions()`. For infinite queries, `infiniteOptions` takes `input` as a function of the page param.\n\n## oRPC v1 to v2\n\nMost v1 names still compile through deprecated aliases (strike-through hints, not errors), so migrate in passes. Order of operations:\n\n1. **Update packages.** Install every `@orpc/*` package from the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`, and so on). Swap renamed ones first: `@orpc/react-query`/`@orpc/vue-query`/`@orpc/solid-query`/`@orpc/svelte-query` all became `@orpc/tanstack-query`; `@orpc/openapi-client` merged into `@orpc/openapi`; `@orpc/react` became `@orpc/next`; `@orpc/otel` became `@orpc/opentelemetry`; the `experimental-` packages were promoted (`@orpc/publisher`, `@orpc/ratelimit`, `@orpc/pino`, `@orpc/swr`); `@orpc/vue-colada` became `@orpc/pinia-colada`. Typecheck: the remaining errors are the hard breaks.\n2. **Fix the hard breaks** (no aliases):\n   - Routing: `.route`, `.prefix`, `.tag`, `.$route` are gone from the builder. Use `.meta(openapi({ method, path, prefix, tags }))` from `@orpc/openapi`, or restore `.route` with `import '@orpc/openapi/extensions/route'`.\n   - `.callable` and `.actionable`: use `call`/`createRouterClient` from `@orpc/server` and `createServerFunctionable` from `@orpc/next`, or the corresponding extension imports.\n   - `RPCLink`: the single `url` split into `origin` plus a path-only `url`.\n   - Errors: `status` was removed from `ORPCError` and `.errors` definitions; map codes to HTTP status with `errorStatusMap` on the handler.\n   - `safe()`: the third tuple element is now the typed error itself (or `null`) and a fourth `isSuccess` element was added.\n   - Option renames, scoped: handler `rootInterceptors` to `routingInterceptors` (handler `clientInterceptors` still exists, unchanged); link `clientInterceptors` to `transportInterceptors`. Flat `eventIterator*` options moved under the adapter's request/response mapping: `toFetchResponse.eventStream` on the fetch handler, `sendStandardResponse.eventStream` on Node, `toFetchRequest.eventStream` on the link.\n   - `adapterInterceptors` was removed from handlers and links, because regular interceptors can now customize body parsing behavior.\n3. **Audit silent behavior changes** (compile fine, behave differently):\n   - **Wire format changed:** a v1 link cannot talk to a v2 server, in either direction. Deploy the upgraded server and clients together.\n   - **Automatic middleware deduplication removed:** middleware applied at both router and procedure level now runs twice, with no warning. Guard shared middleware with the context-flag pattern from the dedupe-middleware recipe.\n   - **Batch Plugin `exclude` became `filter` with the opposite meaning.** Usually delete `exclude`; if skipping is still needed, negate the predicate.\n   - **`RPCHandler` rejects GET by default** (`allowMethods` defaults to POST/PUT/PATCH/DELETE). Simplest fix: stop sending GET from the link; only allow GET deliberately, with CSRF protection.\n   - **Handler `filter` takes positional arguments now;** the v1 destructured form still type-checks but reads wrong values.\n   - **`.input`/`.output` now stack:** a repeated call adds a schema instead of replacing the previous one.\n4. **Sweep deprecated aliases** last: `eventIterator` to `asyncIteratorObject`, handler plugins gained a `HandlerPlugin` suffix and link plugins a `LinkPlugin` suffix, `ContractRouter*` types became `RouterContract*`. The from-v1 guide ends with the full alias cheat sheet.\n\nVerification: typecheck and unit tests after steps 1, 2, and 4; step 3 needs integration or e2e tests, since those changes never surface at compile time. Before finishing, grep for old package names and remaining deprecation strike-throughs.\n\n## Docs retrieval\n\nFetch pages instead of recalling them, and if this skill and a fetched page disagree, trust the page. The v2 docs live at https://orpc.dev and the v1 docs at https://v1.orpc.dev; slugs look alike across both hosts, so check which host a page came from before copying anything from it. The index of every v2 docs page is at https://orpc.dev/llms.txt, https://orpc.dev/llms-full.txt bundles the entire docs in one large file, and appending `.md` to any page URL returns its exact source markdown.\n\nAuthoritative pages to consult during the migration (this skill deliberately omits their full mapping tables):\n\n- https://orpc.dev/docs/migrations/from-trpc : side-by-side tRPC/oRPC code for every step, including server setup per framework\n- https://orpc.dev/docs/migrations/from-v1 : every v2 breaking change with v1/v2 comparisons, package rename table, and the deprecated alias cheat sheet\n- https://orpc.dev/docs/integrations/trpc : `toORPCRouter` and `toTRPCMeta` reference for the incremental path\n\n<!-- File: skills/orpc-openapi/SKILL.md (Tokens: ~2698 | Category: anthropic-skill) -->\n---\nname: orpc-openapi\ndescription: \"Expose an oRPC router as a spec-compliant OpenAPI HTTP API. Use when a project depends on @orpc/openapi, or for defining REST-style routes on oRPC procedures (openapi() metadata or .route with method, path, successStatus), serving them with OpenAPIHandler alongside RPCHandler, coercing query and path strings with Smart Coercion, calling an OpenAPI-shaped API with OpenAPILink, generating an OpenAPI 3.2 (or 3.1, 3.0) document with OpenAPIGenerator, or serving Scalar or Swagger docs with the OpenAPI Reference plugin. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For contract-first work (defining contracts with oc, implementing them with implement, or generating a contract from an existing OpenAPI spec), use the orpc-contract skill; for plain RPC serving, core builder, middleware, or client work with no REST exposure, use the orpc skill instead.\"\nlicense: MIT\n---\n\n# oRPC over OpenAPI\n\noRPC procedures speak two protocols from one router: the RPC protocol (`RPCHandler`/`RPCLink`) and plain OpenAPI HTTP (`OpenAPIHandler`/`OpenAPILink`). This skill covers the OpenAPI side. For core builder, middleware, context, and client concepts, load the `orpc` skill (it also carries the v2 install and version check); for contract-first workflows with `@orpc/contract`, load the `orpc-contract` skill.\n\nThis skill targets oRPC v2. Pretrained oRPC knowledge describes v1 and is often wrong for v2: when unsure of any API below, fetch its exact docs page first (see [Full documentation](#full-documentation)).\n\n## Routing\n\nA procedure defaults to `POST` with a path derived from the router structure (`planet.create` becomes `POST /planet/create`). Override with `openapi` metadata, on server (`os`) and contract (`oc`) builders alike:\n\n```ts\nimport { openapi } from '@orpc/openapi'\nimport { os } from '@orpc/server'\nimport { z } from 'zod'\n\nconst getPlanet = os\n  .meta(openapi({ method: 'GET', path: '/planets/{id}', successStatus: 200 }))\n  .input(z.object({ id: z.string() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n- Path params: put `{id}` in `path` and the same key as a required field in the input schema. Use `{+path}` for catch-all segments that may contain `/`.\n- `prefix` prepends a path to a procedure or a whole router: `os.meta(openapi({ prefix: '/api/v2' })).router({...})`. Always set a `prefix` on lazy routers so lazy loading only triggers for relevant requests.\n- Merging across repeated `.meta(openapi(...))` calls: `prefix` and `tags` concatenate, `method`/`path`/`successStatus` last-wins, and setting a field to `undefined` resets it to the default.\n- `successStatus` defaults to `200` and must be below 400.\n\nFor direct routing without `.meta(openapi(...))`, enable the `.route` extension with a side-effect import in a module that always runs at startup (base builder file or server entry):\n\n```ts\nimport '@orpc/openapi/extensions/route'\n\nconst ping = os.route({ method: 'GET', path: '/ping' }).handler(async () => 'pong')\n```\n\n## Input and output mapping\n\nCompact mode (default): path params merge with query params or the request body depending on the HTTP method, so `GET /planets/earth?q=life` yields input `{ id: 'earth', q: 'life' }`. The handler's return value becomes the response body with status `successStatus`.\n\nSet `inputStructure: 'detailed'` in the metadata to receive `{ params, query, headers, body }` instead (define only the fields you need in the input schema). Set `outputStructure: 'detailed'` to return `{ status?, headers?, body? }` and vary the status per response.\n\nQuery strings and form data are decoded with bracket notation (fetch `openapi/bracket-notation` before designing schemas for nested query input; its limits are not guessable):\n\n- Repeated keys become arrays: `?color=red&color=blue` gives `['red', 'blue']`; `color[]=red` pushes too.\n- `[number]` targets an array index; `[key]` targets an object property: `?filter[status]=active` gives `{ filter: { status: 'active' } }`.\n- It cannot represent empty objects or arrays, root-level arrays, or objects whose keys are all numbers, and query/form values always arrive as strings (or files, in form data).\n\nOverride decoding per parameter with `paramsStyles` (`'primitive'`, `'comma-delimited-array'`, `'comma-delimited-object'`) and `queryStyles` (those plus `'array'`, `'json'`, and space/pipe-delimited variants). Use `requestBodyHint`/`responseBodyHint` (`'json'`, `'form-data'`, `'event-stream'`, `'octet-stream'`, `'file'`, ...) when headers alone cannot tell the parser how to handle the body, for example a raw `ReadableStream` upload.\n\n## Serving: OpenAPIHandler\n\nImport from the adapter subpath (`@orpc/openapi/fetch`, `@orpc/openapi/node`, ...):\n\n```ts\nimport { SmartCoercionHandlerPlugin } from '@orpc/json-schema'\nimport { OpenAPIHandler } from '@orpc/openapi/fetch'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new SmartCoercionHandlerPlugin({ converters: [new ZodToJsonSchemaConverter()] }),\n  ],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, { prefix: '/api', context: {} })\n  return matched ? response : new Response('Not Found', { status: 404 })\n}\n```\n\n`OpenAPIHandler` coexists with `RPCHandler`: both accept the same router, so mount them on different prefixes (for example `/api` and `/rpc`) and try each in turn, returning the first `matched` response.\n\nSmart Coercion: query, path, and form values arrive as strings, so add `SmartCoercionHandlerPlugin` whenever input schemas expect non-string types from those sources. It coerces schema-driven, lossless conversions only (`'123'` to `123`, `'true'`/`'on'` to `true`, ISO strings to `Date`, arrays to `Set`/`Map` via `x-native-type`) and leaves ambiguous values untouched. Skip it when you already coerce in the schema or performance is critical; it adds runtime overhead.\n\nOther handler options: `interceptors`/`routingInterceptors`/`clientInterceptors` for logging and error mapping, `filter` to exclude procedures from matching, and `errorStatusMap` plus `customErrorResponseBodyEncoder` to customize error responses (by default `ORPCError` codes map to statuses via `COMMON_ERROR_STATUS_MAP`, for example `NOT_FOUND` to 404).\n\n## Calling: OpenAPILink\n\n`OpenAPILink` calls an OpenAPI-shaped oRPC API (or any spec-compliant server) through a typesafe client. It needs the contract or router type to know each procedure's route:\n\n```ts\nimport type { RouterContractClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi'\nimport { createORPCClient } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi/fetch'\n\nconst link = new OpenAPILink(contract, {\n  origin: 'https://api.example.com',\n  url: '/api',\n  headers: ({ context }) => ({\n    authorization: context?.token ? `Bearer ${context.token}` : undefined,\n  }),\n})\n\nconst client: JsonifiedClient<RouterContractClient<typeof contract>> = createORPCClient(link)\n```\n\nWith a router instead of a contract, type the client as `JsonifiedClient<RouterClient<typeof router>>` (`RouterClient` from `@orpc/server`). `JsonifiedClient` exists because OpenAPI serialization is one-way: a `Date` returns as a string. Add `SmartCoercionLinkPlugin` from `@orpc/json-schema` (same converters, first argument is the contract) to restore native types on responses, then drop the `JsonifiedClient` wrapper from the client type.\n\nTo ship a contract to clients without bundling server code, minify it to JSON and import it with a cast; the flow is in the `orpc-contract` skill under \"Ship the contract\".\n\n## Spec document and interactive docs\n\n`OpenAPIGenerator` turns a router or contract into an OpenAPI 3.2 document (pass any `3.1.x` or `3.0.x` `version` for tools that stop at an older version; `QUERY` procedures need 3.2). `OpenAPIReferenceHandlerPlugin` serves the spec at `/spec.json` and a Scalar UI at `/` under the handler prefix (change with `specPath`/`docsPath`, or set `provider: 'swagger'` for Swagger UI):\n\n```ts\nimport { OpenAPIGenerator } from '@orpc/openapi'\nimport { OpenAPIReferenceHandlerPlugin } from '@orpc/openapi/plugins'\nimport { ZodToJsonSchemaConverter } from '@orpc/zod'\n\nconst generator = new OpenAPIGenerator({ converters: [new ZodToJsonSchemaConverter()] })\n\nconst handler = new OpenAPIHandler(router, {\n  plugins: [\n    new OpenAPIReferenceHandlerPlugin({\n      spec: () => generator.generate(router, {\n        base: {\n          info: { title: 'Planet API', version: '1.0.0' },\n          servers: [{ url: '/api' }], // absolute URL in production\n        },\n      }),\n    }),\n  ],\n})\n```\n\nEnrich the document through `openapi` metadata: `operationId`, `summary`, `description`, `tags`, `successDescription`, and a `spec` callback that receives the generated operation object and returns an extended one (security requirements, extra responses). Write `spec` and `base` as OpenAPI 3.2 objects even when generating 3.1 or 3.0; the generator downgrades the whole document. Converters also exist for Valibot (`@orpc/valibot`) and ArkType (`@orpc/arktype`); schemas without a matching converter fall back to Standard JSON Schema conversion.\n\nVerify the wiring before declaring success: request `/spec.json` under the handler prefix and one routed endpoint, and confirm the method, path, and status you configured.\n\n## Contract-first\n\nContract-first workflows belong to the `orpc-contract` skill: defining the shape with `oc` from `@orpc/contract`, implementing it with `implement` from `@orpc/server`, consuming the contract from clients, shipping it as minified JSON, and generating it from an existing OpenAPI spec with Hey API. On the OpenAPI side a contract behaves exactly like a router: attach routes with `.meta(openapi({...}))` on `oc` as shown in [Routing](#routing), serve the implemented router with `OpenAPIHandler` as usual, generate the spec document from the contract directly, and give `OpenAPILink` the contract as its runtime value.\n\n## Full documentation\n\nIf this skill and a fetched docs page disagree, trust the page: this skill is a summary and v2 is still moving. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/openapi/routing.md)\n\nPages to fetch when you need details beyond this skill:\n\n- OpenAPI: `openapi/routing`, `openapi/input-and-output-mapping`, `openapi/bracket-notation`, `openapi/serializer`, `openapi/handler`, `openapi/link`, `openapi/specification`, `openapi/scalar`\n- Plugins: `plugins/smart-coercion`, `plugins/openapi-reference`\n\n<!-- File: skills/orpc/SKILL.md (Tokens: ~3408 | Category: anthropic-skill) -->\n---\nname: orpc\ndescription: \"Build, serve, and call end-to-end typesafe APIs with oRPC v2. Use for any task in a project that depends on `@orpc/*` packages, even a one-procedure change: defining procedures with the os builder (.input/.output/.handler, any Standard Schema validator), assembling routers, middleware and context, typesafe errors with ORPCError, serving via RPCHandler on any runtime adapter, calling from server-side clients (call, createRouterClient) or client-side clients (createORPCClient with RPCLink), integrating TanStack Query, or streaming over SSE. Pretrained oRPC knowledge describes v1 and is wrong for v2, so load this skill even when the change looks trivial. Biases toward retrieval from the oRPC docs over pre-trained knowledge. For REST/OpenAPI exposure, prefer the orpc-openapi skill; for contract-first design, the orpc-contract skill; for tRPC or oRPC v1 migrations, the orpc-migrate skill.\"\nlicense: MIT\n---\n\n# oRPC\n\noRPC is a typesafe API framework: write plain TypeScript functions on the server, call them from clients like local functions. Input is validated at runtime, types flow end to end, and there is no code generation step. The same router can also be served as a REST API with an OpenAPI spec.\n\nThis skill targets oRPC v2. Check what is installed before writing code: `npm ls @orpc/server` (or any `@orpc/*` package). A 1.x version means v1, where this skill's guidance does not apply; use the `orpc-migrate` skill to upgrade. v2 currently ships under the `beta` dist-tag (`npm install @orpc/server@beta @orpc/client@beta`; a plain install silently gets v1). If `npm view @orpc/server dist-tags` shows `latest` at 2.x, the beta has ended: install normally.\n\nPretrained oRPC knowledge describes v1 and is often wrong for v2 (routing moved to `.meta(openapi(...))`, `RPCLink` split `url` into `origin` plus a path, automatic middleware dedupe was removed). Prefer retrieval: the index of every docs page is at https://orpc.dev/llms.txt; see [Full documentation](#full-documentation) for the mechanics.\n\nPackage map:\n\n- `@orpc/server`: the `os` builder, routers, middleware, `RPCHandler`, server-side clients (`call`, `createRouterClient`), `implement` for mocks\n- `@orpc/client`: `createORPCClient`, `RPCLink`, `safe`, `createSafeClient`, `isDefinedError`\n- `@orpc/contract`: contract-first API definitions implemented separately from their logic (see the `orpc-contract` skill)\n- `@orpc/openapi`: `OpenAPIHandler`, `OpenAPILink`, OpenAPI 3.2 spec generation\n- Integration packages such as `@orpc/tanstack-query` and `@orpc/nest`\n\n## Define procedures\n\nBuild a procedure with the `os` builder: describe input with a schema, implement with `.handler`. Zod, Valibot, ArkType, and any other [Standard Schema](https://standardschema.dev/) library work for `.input`, `.output`, and error `data`.\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const listPlanets = os\n  .handler(async () => [{ id: 1, name: 'Earth' }]) // no .input: takes no arguments\n\nexport const findPlanet = os\n  .input(z.object({ id: z.number() }))\n  .handler(async ({ input }) => ({ id: input.id, name: 'Earth' }))\n```\n\n`.handler` is the only required step. The full chain, every other step optional:\n\n```ts\nconst example = os\n  .$context<{ headers: Headers }>() // initial context this procedure requires\n  .errors({ NOT_FOUND: {} }) // typed errors\n  .use(requireAuth) // middleware\n  .input(z.object({ id: z.number() }))\n  .output(z.object({ id: z.number(), name: z.string() })) // optional; also speeds up type checking\n  .handler(async ({ input, context, errors }) => ({ id: input.id, name: 'Earth' }))\n```\n\nEvery builder step returns a new instance, so share base builders freely: `const authed = os.use(requireAuth)` then build many procedures from `authed`.\n\n## Assemble a router\n\nA router is a plain object mapping keys to procedures (or nested routers). Do not use the keys `then`, `bind`, `valueOf`, `toString`, `toJSON`.\n\n```ts\nexport const router = {\n  planet: { list: listPlanets, find: findPlanet },\n  admin: os.use(requireAuth).router({ deletePlanet }), // apply shared middleware to a subtree\n  planetLazy: os.lazy(() => import('./planet')), // code-split; module's default export is a router\n}\n```\n\nInfer types with `InferRouterInputs` / `InferRouterOutputs` from `@orpc/server`. Applying `.use` at both router and procedure level can run the same middleware twice; see the dedupe pattern below.\n\n## Middleware and context\n\nContext comes from two places. Initial context is declared with `.$context` and passed explicitly when serving or calling (environment values: headers, env, db). Injected context is added at runtime by middleware via `next({ context })` (runtime values: the authenticated user).\n\n```ts\nimport { ORPCError, os } from '@orpc/server'\n\nconst base = os.$context<{ headers: Headers }>()\n\nconst requireAuth = base.middleware(async ({ context, next }) => {\n  const user = await parseUser(context.headers)\n  if (!user) {\n    throw new ORPCError('UNAUTHORIZED')\n  }\n  return next({ context: { user } }) // handler now sees context.user, typed non-null\n})\n```\n\n`.use` accepts named middleware or inline functions. Middleware registered before `.input` runs before validation, the rest after. Middleware can also declare typed input (`os.middleware(async ({ next }, id: number) => ...)`); adapt mismatched shapes with `.use(mw.adaptInput(input => input.id))`.\n\nBest practice, dedupe expensive middleware: the same middleware can run twice in one call (router-level plus procedure-level `.use`, or a procedure `call`ing another). Cache the result in context:\n\n```ts\nconst authProvider = os\n  .$context<{ headers: Headers, auth?: { id: string }, authLoaded?: boolean }>()\n  .middleware(async ({ context, next }) => {\n    const auth = context.authLoaded ? context.auth : await loadAuth(context.headers)\n    return next({ context: { auth, authLoaded: true } })\n  })\n```\n\n## Typesafe errors\n\nThrow `ORPCError` (a `code` plus optional `message` and `data`). Both `message` and `data` are sent to the client, so never put secrets in them. Throw only `Error` instances, never literals.\n\nDefine errors with `.errors` so clients can infer each error's shape:\n\n```ts\nconst find = os\n  .errors({\n    NOT_FOUND: { message: 'Planet not found' }, // default message\n    RATE_LIMITED: { data: z.object({ retryAfter: z.number() }) },\n  })\n  .handler(async ({ input, errors }) => {\n    throw errors.NOT_FOUND()\n  })\n```\n\n`throw new ORPCError('NOT_FOUND')` inside that handler is converted to the matching typed error when code and data match. Convert custom error classes to `ORPCError` in a middleware `try/catch`.\n\n## Serve with RPCHandler\n\n`RPCHandler` matches requests to procedures, validates input, runs handlers, and encodes results. Pick the adapter for your runtime. Fetch API (Bun, Deno, Cloudflare Workers):\n\n```ts\nimport { onError } from '@orpc/server'\nimport { RPCHandler } from '@orpc/server/fetch'\nimport { CORSHandlerPlugin } from '@orpc/server/plugins'\n\nconst handler = new RPCHandler(router, {\n  plugins: [new CORSHandlerPlugin()],\n  interceptors: [onError(error => console.error(error))],\n})\n\nexport async function fetch(request: Request): Promise<Response> {\n  const { matched, response } = await handler.handle(request, {\n    prefix: '/rpc',\n    context: { headers: request.headers }, // provide the router's initial context here\n  })\n  return matched ? response : new Response('Not found', { status: 404 })\n}\n// Bun.serve({ fetch }) / Deno.serve(fetch) / export default { fetch } on Workers\n```\n\nNode HTTP:\n\n```ts\nimport { createServer } from 'node:http'\nimport { RPCHandler } from '@orpc/server/node'\n\nconst handler = new RPCHandler(router)\n\nconst server = createServer(async (req, res) => {\n  const { matched } = await handler.handle(req, res, { prefix: '/rpc', context: {} })\n  if (matched)\n    return\n  res.statusCode = 404\n  res.end('Not found')\n})\n\nserver.listen(3000)\n```\n\nUnmatched requests fall through to your own handling. By default `RPCHandler` accepts only `POST`, `PUT`, `PATCH`, and `DELETE`; enabling `GET` via `allowMethods` is a CSRF risk with cookie auth, see [RPC Handler](https://orpc.dev/docs/rpc/handler). Handler options also include `interceptors`, `routingInterceptors`, `clientInterceptors`, `plugins`, `filter`, and `errorStatusMap`. Adapters also exist for [AWS Lambda](https://orpc.dev/docs/adapters/aws-lambda), [Fastify](https://orpc.dev/docs/adapters/fastify), [WebSocket](https://orpc.dev/docs/adapters/websocket), [Message Port](https://orpc.dev/docs/adapters/message-port), and [Expo](https://orpc.dev/docs/adapters/expo).\n\n## Call procedures\n\nServer side (same process, no HTTP; also the fastest way to test procedures):\n\n```ts\nimport { call, createRouterClient } from '@orpc/server'\n\nconst planet = await call(findPlanet, { id: 1 }, { context: { headers } })\n\nconst client = createRouterClient(router, { context: { headers } }) // context can be a function\nconst planets = await client.planet.list()\n```\n\nClient side, `RPCLink` turns calls into HTTP requests. Import the router as a type only so no server code reaches the client bundle:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from '../server/router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({\n  origin: 'http://127.0.0.1:3000',\n  url: '/rpc', // must match the server's prefix\n  headers: () => ({ authorization: `Bearer ${token}` }), // options accept functions\n})\n\nexport const orpc: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await orpc.planet.find({ id: 1 })\n```\n\nClient error handling: plain `try/catch` works, but `safe` preserves typed error inference:\n\n```ts\nimport { createSafeClient, isDefinedError, safe } from '@orpc/client'\n\nconst [error, data] = await safe(orpc.planet.find({ id: 1 }))\nif (isDefinedError(error)) {\n  console.log(error.code, error.data) // typed from the procedure's .errors\n}\nelse if (error) {\n  // unknown error\n}\n\nconst safeClient = createSafeClient(orpc) // every call returns [error, data]\n```\n\n## Beyond the basics\n\n- Streaming / SSE: return an async generator from `.handler`, validate events with `asyncIteratorObject`, resume via `lastEventId`: [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object)\n- OpenAPI: serve the same router as REST with routing metadata plus `OpenAPIHandler`, and generate a spec (covered in depth by the `orpc-openapi` skill): [OpenAPI Handler](https://orpc.dev/docs/openapi/handler)\n- Contract-first: define contracts with `@orpc/contract`, implement with `implement` (covered in depth by the `orpc-contract` skill): [Contracts](https://orpc.dev/docs/contract/procedure)\n- Plugins for handler and link: batch, CORS, dedupe, retry, compression, request limits, smart coercion, static files, timeout, tmp file upload, and more. Fetch a plugin's docs page before configuring it; option names are not guessable: [Plugins](https://orpc.dev/docs/plugins/batch)\n- Integrations: [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) (`createTanstackQueryUtils`), [SWR](https://orpc.dev/docs/integrations/swr), [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada), [Next.js](https://orpc.dev/docs/integrations/next), [NestJS](https://orpc.dev/docs/integrations/nest), [AI SDK](https://orpc.dev/docs/integrations/ai-sdk), [MCP](https://orpc.dev/docs/integrations/mcp) (`McpRegistrar` exposes procedures as MCP tools), [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry)\n- Testing: `call` procedures directly; mock with `implement(router.planet.list).handler(() => [])`, and run the project's typecheck before declaring success, since end-to-end types are oRPC's first correctness signal: [Testing and Mocking](https://orpc.dev/docs/recipes/testing-and-mocking)\n- Monorepos: TypeScript project references keep client types resolvable: [Monorepo Setup](https://orpc.dev/docs/recipes/monorepo-setup)\n- Migrating from tRPC or oRPC v1: use the `orpc-migrate` skill\n\n## Full documentation\n\nThis skill is an overview; fetch exact docs instead of guessing APIs, and if this skill and a fetched page disagree, trust the page. The docs are served at https://orpc.dev (the v1 docs stay at https://v1.orpc.dev):\n\n- https://orpc.dev/llms.txt : index of every page with descriptions\n- https://orpc.dev/llms-full.txt : the entire docs in one file (large; prefer single pages)\n- Append `.md` to any docs URL for that page's exact source markdown (for example https://orpc.dev/docs/procedure.md)\n\nDoc map, all under `https://orpc.dev/docs/`:\n\n- Top level: `procedure`, `router`, `middleware`, `context`, `error-handling`, `metadata`, plus `binary-data` (file uploads) and `async-iterator-object` (streaming/SSE)\n- `rpc/*`, `openapi/*`: protocol details, handlers, links; `contract/*`: contract-first (the `orpc-contract` skill)\n- `client/*`: server- and client-side clients, error handling, `DynamicLink`\n- `adapters/*`: per-runtime serving quirks (fetch-api, node-http, aws-lambda, fastify, websocket, message-port, expo)\n- `plugins/*`: twenty handler/link plugins; `helpers/*`: cookie, encryption, form-data, lock, publisher, ratelimit, signing, base64url\n- `integrations/*`: framework glue; `recipes/*`: guidance (testing, SSR, monorepos, validation)\n- `migrations/from-v1`, `migrations/from-trpc`: upgrades (use the `orpc-migrate` skill)\n\n<!-- File: packages/ai-sdk/README.md (Tokens: ~7924 | Category: plugin-manifest) -->\n<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/ai-sdk\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fai-sdk?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n\n<!-- File: packages/arktype/README.md (Tokens: ~7924 | Category: plugin-manifest) -->\n<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/arktype\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Farktype?logo=npm\" />\n  </a>\n  <a href=\"https://app.codspeed.io/middleapi/orpc?utm_source=badge\">\n    <img alt=\"CodSpeed\" src=\"https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n\n<!-- File: packages/bun/README.md (Tokens: ~7868 | Category: plugin-manifest) -->\n<h1 align=\"center\">oRPC - Typesafe APIs Made Simple 🪄</h1>\n\n<div align=\"center\">\n  <a href=\"https://codecov.io/gh/middleapi/orpc\">\n    <img alt=\"codecov\" src=\"https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@orpc/bun\">\n    <img alt=\"weekly downloads\" src=\"https://img.shields.io/npm/dw/%40orpc%2Fbun?logo=npm\" />\n  </a>\n  <a href=\"https://github.com/middleapi/orpc/blob/main/LICENSE\">\n    <img alt=\"MIT License\" src=\"https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative\" />\n  </a>\n  <a href=\"https://discord.gg/TXEbwRBvQn\">\n    <img alt=\"Discord\" src=\"https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff\" />\n  </a>\n  <a href=\"https://deepwiki.com/middleapi/orpc\">\n    <img alt=\"Ask DeepWiki\" src=\"https://img.shields.io/badge/Ask-DeepWiki-blue\" />\n  </a>\n</div>\n\n**oRPC** is a TypeScript framework for building end-to-end typesafe APIs. Write your API once as plain TypeScript functions, and oRPC gives you a fully typed client, a REST API with an OpenAPI document, and MCP tools for AI agents, with no code generation. It runs on Node.js, Bun, Deno, Cloudflare Workers, and more.\n\nDefine procedures on the server and serve them with an [RPC handler](https://orpc.dev/docs/rpc/handler):\n\n```ts\nimport { os } from '@orpc/server'\nimport * as z from 'zod'\n\nexport const router = {\n  planet: {\n    find: os\n      .input(z.object({ id: z.number() }))\n      .handler(({ input }) => ({ id: input.id, name: 'Earth' })),\n  },\n}\n```\n\nThen call them from a client that knows every input and output, without generating anything:\n\n```ts\nimport type { RouterClient } from '@orpc/server'\nimport type { router } from './router'\nimport { createORPCClient } from '@orpc/client'\nimport { RPCLink } from '@orpc/client/fetch'\n\nconst link = new RPCLink({ origin: 'http://localhost:3000', url: '/rpc' })\nconst client: RouterClient<typeof router> = createORPCClient(link)\n\nconst planet = await client.planet.find({ id: 1 }) // { id: number, name: string }\n```\n\n## Documentation\n\nRead the full documentation at [orpc.dev](https://orpc.dev), starting with the [Getting Started guide](https://orpc.dev/docs/getting-started).\n\n## Packages\n\n**Core**\n\n| Package                                                        | Purpose                                            | Docs                                                                                                                                                                                                                                                                     |\n| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |\n| [@orpc/server](https://www.npmjs.com/package/@orpc/server)     | Build APIs or implement contracts.                 | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler)                                         |\n| [@orpc/client](https://www.npmjs.com/package/@orpc/client)     | Consume APIs with end-to-end type safety.          | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling)                              |\n| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi)   | Add OpenAPI compatibility to APIs.                 | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar)                                               |\n\n**Schema validation**\n\n| Package                                                      | Purpose                                                    | Docs                                                  |\n| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |\n| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod)         | JSON Schema converter for [Zod](https://zod.dev/).         | [Zod](https://orpc.dev/docs/integrations/zod)         |\n| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | JSON Schema converter for [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |\n| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | JSON Schema converter for [ArkType](https://arktype.io/).  | [ArkType](https://orpc.dev/docs/integrations/arktype) |\n\n**Built-in features**\n\n| Package                                                                          | Purpose                                                                                                                                                                                      | Docs                                                           |\n| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |\n| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher)                 | Pub/Sub with memory, Redis, and Upstash adapters.                                                                                                                                            | [Publisher](https://orpc.dev/docs/helpers/publisher)           |\n| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit)                 | Rate limiting with memory, Redis, and Upstash adapters.                                                                                                                                      | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit)          |\n| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters.                                                                                                                                            | [Lock](https://orpc.dev/docs/helpers/lock)                     |\n| [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation)             | Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). | [Hibernation](https://orpc.dev/docs/integrations/hibernation)  |\n| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema)             | Smart coercion for OpenAPI requests.                                                                                                                                                         | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |\n\n**Framework & ecosystem integrations**\n\n| Package                                                                              | Purpose                                                                                               | Docs                                                                                                                                                                                                                    |\n| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [@orpc/next](https://www.npmjs.com/package/@orpc/next)                               | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next)                                                                                                                                                                      |\n| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk)                           | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.                               | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk)                                                                                                                                                                     |\n| [@orpc/mcp](https://www.npmjs.com/package/@orpc/mcp)                                 | Expose procedures as [MCP](https://modelcontextprotocol.io/) tools.                                   | [MCP](https://orpc.dev/docs/integrations/mcp)                                                                                                                                                                           |\n| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query)           | Integrate with [TanStack Query](https://tanstack.com/query/latest).                                   | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query)                                                                                                                                                     |\n| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada)               | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).                                         | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada)                                                                                                                                                         |\n| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr)                                 | Integrate with [SWR](https://swr.vercel.app/).                                                        | [SWR](https://orpc.dev/docs/integrations/swr)                                                                                                                                                                           |\n| [@orpc/msw](https://www.npmjs.com/package/@orpc/msw)                                 | Mock procedures with [Mock Service Worker](https://mswjs.io/).                                        | [MSW](https://orpc.dev/docs/integrations/msw)                                                                                                                                                                           |\n| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/).                                                     | [Effect](https://orpc.dev/docs/integrations/effect)                                                                                                                                                                     |\n| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest)                               | Implement your contract with [NestJS](https://nestjs.com/).                                           | [NestJS](https://orpc.dev/docs/integrations/nest)                                                                                                                                                                       |\n| [@orpc/node](https://www.npmjs.com/package/@orpc/node)                               | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.                     | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression)      |\n| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun)                                 | Adapters for [Bun's Redis](https://bun.sh/).                                                          | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock)                                                                 |\n| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare)                   | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/).                        | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |\n| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc)                               | Reuse existing [tRPC](https://trpc.io/) routers within oRPC.                                          | [tRPC](https://orpc.dev/docs/integrations/trpc)                                                                                                                                                                         |\n\n**Observability**\n\n| Package                                                                  | Purpose                                                                            | Docs                                                              |\n| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |\n| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |\n| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino)                   | Integrate with [Pino](https://getpino.io/) for logging.                            | [Pino](https://orpc.dev/docs/integrations/pino)                   |\n| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog)                 | Integrate with [Evlog](https://evlog.dev/) for logging.                            | [Evlog](https://orpc.dev/docs/integrations/evlog)                 |\n\n## Sponsors\n\nLike what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! 🚀\n\n<table>\n  <tr>\n   <td width=\"2000\"><a href=\"https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"The screenshot API for developers\"><img src=\"https://avatars.githubusercontent.com/u/97035603?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"ScreenshotOne.com\"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://yuzu.health/careers?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"We&#39;re hiring NYC based engineers\"><img src=\"https://avatars.githubusercontent.com/u/102488956?v=4\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"Yuzu\"/><b>Yuzu</b></a><br /><sub>We&#39;re hiring NYC based engineers</sub></td>\n  </tr>\n  <tr>\n   <td width=\"2000\"><a href=\"https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Decentralized microblogging SNS born on Earth\"><img src=\"https://github.com/MisskeyIO.png\" width=\"64\" align=\"left\" hspace=\"12\" alt=\"MisskeyHQ\"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>\n  </tr>\n</table>\n\n### Organization Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"LN Markets\"><img src=\"https://avatars.githubusercontent.com/u/70597625?v=4\" width=\"209\" alt=\"LN Markets\"/><br />LN Markets</a></td>\n  </tr>\n</table>\n\n### Sponsors\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Reece McDonald\"><img src=\"https://avatars.githubusercontent.com/u/39349270?v=4\" width=\"167\" alt=\"Reece McDonald\"/><br />Reece McDonald</a></td>\n   <td align=\"center\"><a href=\"https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"あわわわとーにゅ\"><img src=\"https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4\" width=\"167\" alt=\"あわわわとーにゅ\"/><br />あわわわとーにゅ</a></td>\n   <td align=\"center\"><a href=\"https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"nk\"><img src=\"https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4\" width=\"167\" alt=\"nk\"/><br />nk</a></td>\n   <td align=\"center\"><a href=\"https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"supastarter\"><img src=\"https://avatars.githubusercontent.com/u/110960143?v=4\" width=\"167\" alt=\"supastarter\"/><br />supastarter</a></td>\n   <td align=\"center\"><a href=\"https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"herrfugbaum\"><img src=\"https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4\" width=\"167\" alt=\"herrfugbaum\"/><br />herrfugbaum</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryota Murakami\"><img src=\"https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4\" width=\"167\" alt=\"Ryota Murakami\"/><br />Ryota Murakami</a></td>\n   <td align=\"center\"><a href=\"https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Cramer\"><img src=\"https://avatars.githubusercontent.com/u/23610?v=4\" width=\"167\" alt=\"David Cramer\"/><br />David Cramer</a></td>\n   <td align=\"center\"><a href=\"https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Petryniak\"><img src=\"https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4\" width=\"167\" alt=\"Valerii Petryniak\"/><br />Valerii Petryniak</a></td>\n   <td align=\"center\"><a href=\"https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Valerii Strilets\"><img src=\"https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4\" width=\"167\" alt=\"Valerii Strilets\"/><br />Valerii Strilets</a></td>\n   <td align=\"center\"><a href=\"https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kyle Mistele\"><img src=\"https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4\" width=\"167\" alt=\"Kyle Mistele\"/><br />Kyle Mistele</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"christ12938\"><img src=\"https://avatars.githubusercontent.com/u/25758598?v=4\" width=\"167\" alt=\"christ12938\"/><br />christ12938</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ryan Soderberg\"><img src=\"https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4\" width=\"167\" alt=\"Ryan Soderberg\"/><br />Ryan Soderberg</a></td>\n   <td align=\"center\"><a href=\"https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"shota\"><img src=\"https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4\" width=\"167\" alt=\"shota\"/><br />shota</a></td>\n   <td align=\"center\"><a href=\"https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Ellis Driscoll\"><img src=\"https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4\" width=\"167\" alt=\"Ellis Driscoll\"/><br />Ellis Driscoll</a></td>\n   <td align=\"center\"><a href=\"https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Hoang Nguyen\"><img src=\"https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4\" width=\"167\" alt=\"Hoang Nguyen\"/><br />Hoang Nguyen</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://opencollective.com/guest-ac41de3b?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Orestis Ioannou\"><img src=\"https://images.opencollective.com/guest-ac41de3b/avatar/460.png\" width=\"167\" alt=\"Orestis Ioannou\"/><br />Orestis Ioannou</a></td>\n   <td align=\"center\"><a href=\"https://automatio.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Stefan Smiljkovic\"><img src=\"https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&amp;v=4\" width=\"167\" alt=\"Stefan Smiljkovic\"/><br />Stefan Smiljkovic</a></td>\n  </tr>\n</table>\n\n### Backers\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"David Walsh\"><img src=\"https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4\" width=\"139\" alt=\"David Walsh\"/><br />David Walsh</a></td>\n   <td align=\"center\"><a href=\"https://github.com/IPv4Addr?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"IPv4Addr\"><img src=\"https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&amp;v=4\" width=\"139\" alt=\"IPv4Addr\"/><br />IPv4Addr</a></td>\n   <td align=\"center\"><a href=\"https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Robbe Vaes\"><img src=\"https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4\" width=\"139\" alt=\"Robbe Vaes\"/><br />Robbe Vaes</a></td>\n   <td align=\"center\"><a href=\"https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Aidan Sunbury\"><img src=\"https://avatars.githubusercontent.com/u/64103161?v=4\" width=\"139\" alt=\"Aidan Sunbury\"/><br />Aidan Sunbury</a></td>\n   <td align=\"center\"><a href=\"https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"soonoo\"><img src=\"https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4\" width=\"139\" alt=\"soonoo\"/><br />soonoo</a></td>\n   <td align=\"center\"><a href=\"https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kevin Porten\"><img src=\"https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4\" width=\"139\" alt=\"Kevin Porten\"/><br />Kevin Porten</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Denis\"><img src=\"https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4\" width=\"139\" alt=\"Denis\"/><br />Denis</a></td>\n   <td align=\"center\"><a href=\"https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Christopher Kapic\"><img src=\"https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4\" width=\"139\" alt=\"Christopher Kapic\"/><br />Christopher Kapic</a></td>\n   <td align=\"center\"><a href=\"http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Tom Ballinger\"><img src=\"https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4\" width=\"139\" alt=\"Tom Ballinger\"/><br />Tom Ballinger</a></td>\n   <td align=\"center\"><a href=\"https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Sam\"><img src=\"https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4\" width=\"139\" alt=\"Sam\"/><br />Sam</a></td>\n   <td align=\"center\"><a href=\"https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Titoine\"><img src=\"https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4\" width=\"139\" alt=\"Titoine\"/><br />Titoine</a></td>\n   <td align=\"center\"><a href=\"https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Igor Makowski\"><img src=\"https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4\" width=\"139\" alt=\"Igor Makowski\"/><br />Igor Makowski</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"hanayashiki\"><img src=\"https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4\" width=\"139\" alt=\"hanayashiki\"/><br />hanayashiki</a></td>\n   <td align=\"center\"><a href=\"https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Lev Dubinets\"><img src=\"https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4\" width=\"139\" alt=\"Lev Dubinets\"/><br />Lev Dubinets</a></td>\n   <td align=\"center\"><a href=\"https://kellychan.im/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Kelly Peilin Chan\"><img src=\"https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4\" width=\"139\" alt=\"Kelly Peilin Chan\"/><br />Kelly Peilin Chan</a></td>\n   <td align=\"center\"><a href=\"https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Guy Ariely\"><img src=\"https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4\" width=\"139\" alt=\"Guy Ariely\"/><br />Guy Ariely</a></td>\n   <td align=\"center\"><a href=\"https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"PaulSenon\"><img src=\"https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4\" width=\"139\" alt=\"PaulSenon\"/><br />PaulSenon</a></td>\n   <td align=\"center\"><a href=\"https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Shotaro Nakamura\"><img src=\"https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4\" width=\"139\" alt=\"Shotaro Nakamura\"/><br />Shotaro Nakamura</a></td>\n  </tr>\n  <tr>\n   <td align=\"center\"><a href=\"https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Alex\"><img src=\"https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4\" width=\"139\" alt=\"Alex\"/><br />Alex</a></td>\n   <td align=\"center\"><a href=\"https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor\" target=\"_blank\" rel=\"noopener sponsored\" title=\"Andrey Gubanov\"><img src=\"https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4\" width=\"139\" alt=\"Andrey Gubanov\"/><br />Andrey Gubanov</a></td>\n  </tr>\n</table>\n\nWith thanks to [40 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.\n\n## Open Source Programs\n\n[middleapi](https://github.com/middleapi) is supported by these open source programs:\n\n<table>\n  <tr>\n   <td align=\"center\"><a href=\"https://claude.com/contact-sales/claude-for-oss\" target=\"_blank\" rel=\"noopener\" title=\"Claude for Open Source\"><img src=\"https://cdn.simpleicons.org/claude\" width=\"64\" alt=\"Claude\"/><br />Claude for Open Source</a></td>\n  </tr>\n</table>\n\n## References\n\noRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:\n\n- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.\n- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.\n\n## License\n\nDistributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.\n\n<!-- ... +26 more modular skills available via /api/skills/middleapi/orpc?path={path} -->\n</yakaai_skills>"}