mirror of
https://github.com/prdlk/skills.git
synced 2026-11-02 13:12:48 +00:00
init: Add pre-existing skills
This commit is contained in:
commit
434258209a
1173 files changed
+225805
No files matched your search
@@ -0,0 +1,256 @@
|
|||||||
|
---
|
||||||
|
name: absurd
|
||||||
|
description: Debug and operate Absurd durable workflows with absurdctl. Use when working with Absurd queues, tasks, runs, events, retries, schema setup, or when a user wants to inspect, spawn, retry, cancel, or wake workflows.
|
||||||
|
license: Apache-2.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Absurd
|
||||||
|
|
||||||
|
Use this skill when the project uses **Absurd**, the Postgres-native durable workflow engine, or when the user mentions **`absurdctl`**, queues, durable tasks, runs, retries, sleeping tasks, or events.
|
||||||
|
|
||||||
|
## Tiny mental model
|
||||||
|
|
||||||
|
- A **queue** is a namespace of Absurd tables (`t_`, `r_`, `c_`, `e_`, `w_`).
|
||||||
|
- A **task** is the durable workflow instance.
|
||||||
|
- A **run** is one execution attempt of a task.
|
||||||
|
- A **step** is a checkpoint. Completed step results are stored as JSON.
|
||||||
|
- **Sleeping** tasks are usually waiting for time or an event.
|
||||||
|
- **Events** wake waiting tasks. Event payloads are cached; first emit wins.
|
||||||
|
|
||||||
|
Important distinction:
|
||||||
|
|
||||||
|
- `task_id` = the whole workflow across all attempts
|
||||||
|
- `run_id` = one specific execution attempt
|
||||||
|
|
||||||
|
## First: make sure `absurdctl` works
|
||||||
|
|
||||||
|
If `absurdctl` is not on `PATH`, check whether you are inside a repo checkout and, if so, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$PWD:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Absurd connection precedence is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--database > ABSURD_DATABASE_URL > PGDATABASE > postgresql://localhost/absurd
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-URI connections, `PGHOST`, `PGPORT`, `PGUSER`, and `PGPASSWORD` are also honored.
|
||||||
|
|
||||||
|
## Default debugging workflow
|
||||||
|
|
||||||
|
Prefer **`absurdctl` state inspection before source inspection**. Usually you do **not** need to read application code first.
|
||||||
|
|
||||||
|
If the user explicitly asks you to use **`absurdctl`** to inspect or fix a workflow, do that first instead of starting with `rg` / source browsing.
|
||||||
|
|
||||||
|
When the user wants to debug a task, start with these commands in order:
|
||||||
|
|
||||||
|
### 1) Discover queues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl list-queues
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Inspect recent activity in the likely queue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl list-tasks --queue=default --limit=20
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `list-tasks` defaults to 50 rows if `--limit` is omitted.
|
||||||
|
- Useful statuses: `pending`, `running`, `sleeping`, `completed`, `failed`, `cancelled`.
|
||||||
|
|
||||||
|
### 3) Focus on failures or sleepers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl list-tasks --queue=default --status=failed --limit=20
|
||||||
|
absurdctl list-tasks --queue=default --status=sleeping --limit=20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4) Inspect one workflow or one attempt in detail
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl dump-task --task-id=<task-id>
|
||||||
|
absurdctl dump-task --run-id=<run-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
`dump-task` is the most important inspection command. It shows things like:
|
||||||
|
- task name, params, and headers
|
||||||
|
- retry settings and attempts
|
||||||
|
- checkpointed step state
|
||||||
|
- waits / events / sleep state
|
||||||
|
- final result or failure
|
||||||
|
|
||||||
|
## How to reason about common states
|
||||||
|
|
||||||
|
### If a task is `failed`
|
||||||
|
|
||||||
|
1. `dump-task --task-id=<task-id>`
|
||||||
|
2. Read the failure and the last successful checkpoints.
|
||||||
|
3. If needed, inspect the most recent attempt with `--run-id`.
|
||||||
|
4. Search the code for the task implementation by task name.
|
||||||
|
5. Only then decide whether to retry.
|
||||||
|
|
||||||
|
### If a task is `sleeping`
|
||||||
|
|
||||||
|
1. `dump-task --task-id=<task-id>`
|
||||||
|
2. Look for the wait reason:
|
||||||
|
- sleeping until a timestamp
|
||||||
|
- waiting for an event name
|
||||||
|
3. If it is waiting for an event and the user wants it resumed, emit that event.
|
||||||
|
|
||||||
|
### If a task is `running`
|
||||||
|
|
||||||
|
1. `dump-task --task-id=<task-id>`
|
||||||
|
2. Look at existing checkpoints to see how far it got.
|
||||||
|
3. If the user suspects a stuck worker, inspect the worker process / application logs too.
|
||||||
|
|
||||||
|
### About workers
|
||||||
|
|
||||||
|
Do not assume you need to start or modify a worker.
|
||||||
|
|
||||||
|
- If a spawned task moves from `pending` to `sleeping`, `running`, or `completed`, a worker is already active.
|
||||||
|
- If tasks remain `pending`, then investigate whether a worker for that queue is actually running.
|
||||||
|
- Only inspect Python / TypeScript runtime details when the task state suggests a worker problem or the user asks for code changes.
|
||||||
|
|
||||||
|
### If you need the implementation
|
||||||
|
|
||||||
|
After you know the task name, search the codebase for its registration.
|
||||||
|
|
||||||
|
TypeScript / JavaScript:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "registerTask\(|name:\s*['\"]<task-name>['\"]" .
|
||||||
|
```
|
||||||
|
|
||||||
|
Python:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "register_task\(|@.*register_task|['\"]<task-name>['\"]" .
|
||||||
|
```
|
||||||
|
|
||||||
|
If the task is waiting for an event, also search for the event name.
|
||||||
|
|
||||||
|
## Common actions
|
||||||
|
|
||||||
|
### Spawn work
|
||||||
|
|
||||||
|
Use `-P key=value` for strings and `-P key:=json` for typed JSON values.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl spawn-task my-task -q default -P foo=bar
|
||||||
|
absurdctl spawn-task my-task -q default -P count:=42 -P enabled:=true
|
||||||
|
absurdctl spawn-task my-task -q default -P user.name=Alice -P user.age:=30
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--params` when the user already has a JSON object:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl spawn-task my-task -q default --params '{"foo":"bar","count":42}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry failed work
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl retry-task <task-id>
|
||||||
|
absurdctl retry-task <task-id> --max-attempts 5
|
||||||
|
absurdctl retry-task -q default <task-id> --spawn-new
|
||||||
|
```
|
||||||
|
|
||||||
|
Guidance:
|
||||||
|
- plain `retry-task` retries the existing task
|
||||||
|
- `--spawn-new` creates a brand-new task with the original inputs
|
||||||
|
- prefer understanding the failure before retrying
|
||||||
|
|
||||||
|
### Cancel work
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl cancel-task <task-id>
|
||||||
|
absurdctl cancel-task -q default <task-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wake waiting tasks by emitting an event
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl emit-event order.completed -q default -P orderId=123
|
||||||
|
absurdctl emit-event approval.granted:42 -q default -P approved:=true
|
||||||
|
```
|
||||||
|
|
||||||
|
If the event payload should be structured JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl emit-event shipment.packed:42 -q default --payload '{"trackingNumber":"XYZ"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema setup / migrations
|
||||||
|
|
||||||
|
Use these on a blank or controlled database, or when the user explicitly asks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl init
|
||||||
|
absurdctl schema-version
|
||||||
|
absurdctl migrate
|
||||||
|
absurdctl create-queue default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safe operating rules
|
||||||
|
|
||||||
|
Be careful with state-changing commands. Unless the user clearly wants them, avoid running these blindly on a shared or production database:
|
||||||
|
|
||||||
|
- `init`
|
||||||
|
- `migrate`
|
||||||
|
- `create-queue`
|
||||||
|
- `drop-queue`
|
||||||
|
- `cleanup`
|
||||||
|
- `cancel-task`
|
||||||
|
- `retry-task`
|
||||||
|
- `emit-event`
|
||||||
|
- `spawn-task`
|
||||||
|
|
||||||
|
If the environment is ambiguous, ask which database / queue is safe to operate on.
|
||||||
|
|
||||||
|
## Good copy-paste playbooks
|
||||||
|
|
||||||
|
### Debug the latest failures in `default`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl list-queues
|
||||||
|
absurdctl list-tasks --queue=default --status=failed --limit=20
|
||||||
|
absurdctl dump-task --task-id=<task-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find sleepers and wake one
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl list-tasks --queue=default --status=sleeping --limit=20
|
||||||
|
absurdctl dump-task --task-id=<task-id>
|
||||||
|
absurdctl emit-event <event-name> -q default -P key=value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reproduce by spawning a task, then inspect it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl spawn-task my-task -q default -P foo=bar
|
||||||
|
absurdctl list-tasks --queue=default --task-name=my-task --limit=5
|
||||||
|
absurdctl dump-task --task-id=<task-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fast path when the user says “spawn a task and debug it”:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
absurdctl spawn-task my-task -q default -P foo=bar
|
||||||
|
absurdctl list-tasks --queue=default --task-name=my-task --limit=5
|
||||||
|
absurdctl dump-task --task-id=<task-id>
|
||||||
|
# then either:
|
||||||
|
absurdctl emit-event <event-name> -q default -P key=value
|
||||||
|
# or:
|
||||||
|
absurdctl retry-task <task-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extra reference
|
||||||
|
|
||||||
|
- Use `absurdctl <command> --help` for full options.
|
||||||
|
- `dump-task --task-id` is usually the best starting point once you know the task.
|
||||||
|
- Checkpointed step results are durable JSON state; code outside steps may execute multiple times across retries.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
name: blume-migrate
|
||||||
|
description: Migrate an existing documentation site (Mintlify, Docusaurus, Fumadocs, Nextra, Starlight, 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, or an astro.config.* with starlight().
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migrate to Blume
|
||||||
|
|
||||||
|
Blume 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`.
|
||||||
|
|
||||||
|
Your 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.
|
||||||
|
|
||||||
|
Throughout 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.
|
||||||
|
|
||||||
|
## Migration philosophy
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **Drop chrome that has no Blume equivalent — and say so.** Navbar CTAs, footer copyright text, custom theming, conditional redirects, and unmappable icons get reported to the user, not silently discarded or faked.
|
||||||
|
- **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."
|
||||||
|
|
||||||
|
## Migration workflow
|
||||||
|
|
||||||
|
1. **Detect the source framework** and read its reference file:
|
||||||
|
- `docs.json` / `mint.json` → **Mintlify** (`references/mintlify.md`) — the deepest, config-declared nav.
|
||||||
|
- `docusaurus.config.*` → **Docusaurus** (`references/docusaurus.md`).
|
||||||
|
- `meta.json` + `fumadocs-*` deps (content under `content/docs/`) → **Fumadocs** (`references/fumadocs.md`).
|
||||||
|
- `_meta.{js,ts,json}` + `nextra` deps → **Nextra** (`references/nextra.md`).
|
||||||
|
- `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).
|
||||||
|
- Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.
|
||||||
|
- **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.
|
||||||
|
2. **Inventory the repo** before changing anything: 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.
|
||||||
|
3. **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: "…" })`.
|
||||||
|
4. **Restructure content.** 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.** 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.
|
||||||
|
5. **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); 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); **add a `redirects` entry for every route you moved** in step 4; **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 `##`). **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`).
|
||||||
|
6. **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. **Always regenerate the lockfile in the same change:** after editing deps run a plain `pnpm install` (from the workspace root) and commit `pnpm-lock.yaml` alongside `package.json` — CI/Vercel use `--frozen-lockfile`, so a stale lockfile fails the build before it starts. **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.67.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.
|
||||||
|
7. **Wire up the host repo & deploy (non-trivial repos).** For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`, and tell the user the two settings you can't commit (Vercel Root Directory, Node 22). 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.
|
||||||
|
8. **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 invalid pages) and `blume validate --strict` (internal links, heading anchors, assets — 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).
|
||||||
|
|
||||||
|
## The Blume mental model
|
||||||
|
|
||||||
|
The single biggest shift for most sources — especially Mintlify — is that **navigation is derived from the filesystem**, not declared in config.
|
||||||
|
|
||||||
|
### Navigation is the file tree
|
||||||
|
|
||||||
|
- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.
|
||||||
|
- **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).
|
||||||
|
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display, directory })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); 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.
|
||||||
|
- **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).
|
||||||
|
- **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 }`).
|
||||||
|
- **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 you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so 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.
|
||||||
|
|
||||||
|
### Tabs and selectors
|
||||||
|
|
||||||
|
- **`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.
|
||||||
|
- **`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.
|
||||||
|
- **`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`.
|
||||||
|
- **`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`.
|
||||||
|
|
||||||
|
### Routes and pathing
|
||||||
|
|
||||||
|
- A route is the content path relative to `content.root`, with **numeric prefixes stripped** (`01-intro.mdx` → `/intro`) and **`(group)/` folders adding no segment**. An `index` file maps to its folder's route. Frontmatter `slug` overrides the generated route.
|
||||||
|
|
||||||
|
### `blume.config.ts` shape
|
||||||
|
|
||||||
|
`defineConfig({...})` — every field optional, all with defaults:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **`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. 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.
|
||||||
|
- **`content`:** `root` (default `"docs"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `["**/*.{md,mdx}"]` / `["**/_*", "**/.*"]`), `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.
|
||||||
|
- **`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).
|
||||||
|
- **`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 header mark 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).
|
||||||
|
- **`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`).
|
||||||
|
- **`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.
|
||||||
|
- **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, Netlify, and Cloudflare Pages**, and uses the dev server's `localhost` URL during `blume dev`; on those three hosts leave it unset and let detection pick the deployed URL. Everywhere else — 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 three 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.
|
||||||
|
- **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-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.
|
||||||
|
|
||||||
|
The 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).
|
||||||
|
|
||||||
|
### Icons are Lucide, period
|
||||||
|
|
||||||
|
Blume 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 defaults to 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.
|
||||||
|
|
||||||
|
### Page frontmatter (strict — unknown keys are build errors)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: Install # renders as the page H1 — remove any duplicate H1 in the body
|
||||||
|
description: Install Blume and scaffold your first project.
|
||||||
|
type: doc # doc (default); blog and changelog drive feeds, other values only mean something under content.types
|
||||||
|
sidebar:
|
||||||
|
label: Install # overrides title in the sidebar
|
||||||
|
order: 2
|
||||||
|
icon: download
|
||||||
|
badge: New
|
||||||
|
hidden: false
|
||||||
|
seo:
|
||||||
|
title: …
|
||||||
|
description: …
|
||||||
|
image: /og/install.png
|
||||||
|
canonical: https://…
|
||||||
|
noindex: false
|
||||||
|
search:
|
||||||
|
exclude: false
|
||||||
|
tags: [api]
|
||||||
|
slug: install # override the generated route
|
||||||
|
draft: false
|
||||||
|
lastModified: 2026-06-20 # pin the "last updated" date
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Also valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog metadata), `deprecated`, `hidden`, `noindex`.
|
||||||
|
|
||||||
|
### Authoring features (no imports needed in `.mdx`)
|
||||||
|
|
||||||
|
- **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. **Rename any `.md` file that uses (or should use) these to `.mdx` during migration.** 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`.
|
||||||
|
- **Callouts as directives:** `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::info`, `:::success`, with an optional title in brackets: `:::warning[Heads up]`. Aliases `caution`→warning, `error`→danger, `important`→note, `warn`→warning.
|
||||||
|
- **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`. (**Not** shipped — convert away: `<Warning>` → the `:::warning` directive, and the `ParamField`/`ResponseField`/`RequestField` field family → `TypeTable` rows or the OpenAPI reference. See the reference files for targets.)
|
||||||
|
- **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 ++]`).
|
||||||
|
- **Math:** `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field) — as a block on lines of its own, or inline within a sentence. A single `$` always stays literal text, so convert each inline `$…$` to `$$…$$` in place, inside its sentence (not onto a line of its own), checking every pair is math and not currency or a shell variable.
|
||||||
|
|
||||||
|
### OpenAPI
|
||||||
|
|
||||||
|
`reference: [openapi({ sources: [{ spec, label?, route? }] })]` — the `openapi()` adapter imported from `blume/reference` (`spec` is the single-source shorthand) — generates **one real page per operation** — with routing, sidebar, search, and OG images for free. **The reference does not get a header tab automatically** — add a `navigation.tabs` entry pointing at the adapter's `route` (reference routes are valid tab targets) or the API reference is unreachable from the header. **Never hand-migrate generated API-reference pages** (per-endpoint stub pages in the source): delete them and point `openapi()` at the spec. To keep a source's **Scalar embed** instead, list `scalar({ spec, theme?, …scalarOptions })` (also from `blume/reference`) in `reference` in place of `openapi()`: it renders an OpenAPI or AsyncAPI document as one embedded page per source, forwards every key it doesn't name verbatim to Scalar, and doesn't take the native display options (`codeSamples`, `expandSchemas`, `playground`). There is **no** `renderer` option on `openapi()`/`asyncapi()` — it fails validation. **Blume 1.x's top-level `openapi`/`asyncapi`/`graphql` blocks are gone** — when a source config (or an older `blume.config.ts`) has `openapi: { enabled: true, ... }`, rewrite it as an entry in `reference` and drop `enabled`; a 1.x block with `renderer: "scalar"` becomes its own `scalar({ … })` entry instead, keeping the block's `route`, `sources`, and `noindex`, with its `theme` and the keys of its `scalar: { … }` object passed straight to `scalar()` (`scalar({ spec, theme: "purple", localization })`).
|
||||||
|
|
||||||
|
- **Vendor the spec by default.** A remote `spec:` URL makes every build depend on fetching it at build time — a single point of failure in CI, offline, or behind a proxy, and a failed fetch skips the whole reference. Prefer committing the spec into the repo (`openapi/<name>.json`) and pointing `spec` at the local path; if you keep the URL, say so and consider a `prebuild` step that refreshes the local copy with a fallback.
|
||||||
|
- **Operation routes have their own slug scheme** — `<route>/<slugified-tag>/<slugified-operationId>` (e.g. tag `Models`, id `listModels` → `/api-reference/models/list-models`). A camelCase operation id is split at its word boundaries into kebab-case before slugifying (`getHTTPResponse` → `get-http-response`), so don't just lowercase it; an operation with no `operationId` takes its method and path instead (`GET /pets/{id}` → `get-pets-id`). This rarely matches the source's endpoint links (Mintlify/others kebab-case differently), so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route, so it catches the ones you miss.
|
||||||
|
- **Keep hand-written conceptual pages.** Sources often pair a written "Introduction/Authentication" page with the endpoint group in the same tab. A normal content page placed under the `openapi()` adapter's `route` merges into the reference tab's sidebar — so keep those (auth, errors, rate limits) and delete only the per-endpoint stubs.
|
||||||
|
|
||||||
|
### GraphQL
|
||||||
|
|
||||||
|
`reference: [graphql({ spec, endpoint? })]` is the GraphQL counterpart: Blume lowers a schema — SDL text or an introspection JSON result, local path or URL — into **one real page per root field** (grouped as Queries/Mutations/Subscriptions) plus **one page per named type** (Objects, Input Objects, Enums, Interfaces, Unions, custom Scalars). Every OpenAPI rule above carries over: never hand-migrate a source's generated GraphQL reference pages (delete them and point `spec` at the schema), vendor a remote schema locally, keep hand-written conceptual pages under the `route` (default `/graphql`), add the `navigation.tabs` entry, and rewrite inbound links — routes are `<route>/<slugified-group>/<slugified-name>` (e.g. `/graphql/queries/pets`, `/graphql/objects/pet`), which `blume validate` resolves like any other page. Differences from the `openapi()` adapter:
|
||||||
|
|
||||||
|
- **Set `endpoint` to the live GraphQL URL.** A schema, unlike an OpenAPI document, names no server — `endpoint` is what the Try It playground and code samples target (without it they render a placeholder URL, and `playground: { proxy: true }` warns at build time because the proxy has no origin to allow). Multiple schemas use `sources: [{ spec, endpoint?, label?, route? }]`; a per-source `endpoint` overrides the adapter-level one.
|
||||||
|
- **No Scalar embed** — there is no `scalar()` counterpart for a GraphQL schema (and `graphql()` takes no `expandSchemas`); the reference is always Blume-rendered (the Scalar embed reads OpenAPI and AsyncAPI documents only). A source's GraphQL playground/explorer embed (GraphiQL, Apollo Explorer) has no direct equivalent beyond the built-in Try It panel; report anything it did that the panel doesn't.
|
||||||
|
- **Only SDL and introspection JSON are accepted.** A source that builds its schema programmatically (a `GraphQLSchema` instance in code) must be printed to SDL (`printSchema` from `graphql`) and committed; report that conversion.
|
||||||
|
|
||||||
|
### Changelogs
|
||||||
|
|
||||||
|
If the source ships a **hand-maintained changelog** (a `changelog.mdx`, a folder of dated entries, Mintlify `<Update>` blocks) **and the project is open source on GitHub**, offer to replace it with the **`githubReleases()`** content source — release notes become the changelog automatically, with no files to maintain. It's an offer, not an automatic rewrite: some teams keep a curated changelog that doesn't map 1:1 to GitHub releases, so confirm the release notes are the source of truth before deleting their pages.
|
||||||
|
|
||||||
|
Add it under `content.sources` alongside the filesystem source (both imported from `blume/sources`; with `sources` present, the content root moves into `filesystem()`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { filesystem, githubReleases } from "blume/sources";
|
||||||
|
|
||||||
|
content: {
|
||||||
|
sources: [
|
||||||
|
filesystem({ include: ["docs/**/*.mdx"], root: "." }),
|
||||||
|
githubReleases({
|
||||||
|
owner: "haydenbleasel",
|
||||||
|
repo: "ultracite",
|
||||||
|
prefix: "changelog",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- Each release materializes as a `type: changelog` page under `/<prefix>/` (`prefix: "changelog"` → `/changelog/…`); omit `prefix` to mount at the root.
|
||||||
|
- Optional fields: `limit` (cap materialized releases, newest-first, default 100), `prereleases` (include prereleases), `drafts` (include drafts — needs a token with repo write access), `pollInterval` (dev polling seconds; omit to freeze for the session).
|
||||||
|
- A **private** repo reads a token from `GITHUB_TOKEN`; it is never inlined in config. The adapter declares it, so `blume dev`/`build` warn when it is unset; a public repo still works without it.
|
||||||
|
- **Delete the old changelog pages** once the source is wired (and add `redirects` from their old routes to the new `/<prefix>/…` slugs). Pin a header/sidebar link with `navigation.featured` if the source had one.
|
||||||
|
|
||||||
|
### Redirects are static
|
||||||
|
|
||||||
|
A `redirects: [{ from, to, status? }]` array **in `blume.config.ts`** maps old URLs when you restructure routes — Blume serves these itself, so any reorganization that moves a page (folder-per-tab, materialized nested groups, renamed slugs, index promotion) is fixed by adding an entry there; no host config needed. **Restructuring is the main source of these:** every page you moved in step 4 (folder-per-tab, renamed slugs, index promotion) needs an entry, or old URLs 404. `status` defaults to **301 (permanent — browsers cache it indefinitely)**; that's correct for genuine moves, but never use 301/308 for redirects you might reverse. Patterns carry over as written: `:name` for one segment, `:name*` or a trailing `*` for the rest of the path (`{ from: "/beta/:slug*", to: "/v2/:slug*" }`). A pattern can't also match a page, so the build fails (`BLUME_REDIRECT_MATCHES_PAGE`) if one covers a page you kept; narrow it. Rules a pattern can't express (regex params, header or cookie conditions) move to host-level config (`_redirects`, `vercel.json`); report them.
|
||||||
|
|
||||||
|
## Verification & reporting
|
||||||
|
|
||||||
|
1. Run **`blume build`** — it validates the frontmatter schema, duplicate routes, and config, and fails on any error diagnostic by default (**don't pass `--no-strict`**: that builds anyway and silently drops invalid pages). Then run **`blume validate --strict`** — links, heading anchors, and assets live here, not in `build` (add `--external` to also check outbound HTTP links). OpenAPI operation pages are real routes to `validate`, so dead links to them are caught too. Iterate until both are clean.
|
||||||
|
2. Run `blume dev` and review the site visually — nav structure, tabs, theme, rendered components.
|
||||||
|
3. **Write a migration summary** covering: what was migrated (config, N pages, nav, API references), what was **dropped** (navbar CTAs, footer copyright text, custom theming, conditional redirects, unmappable icons, unsupported components), and suggested follow-ups (`blume eject` for full control, `blume add` to vendor a component for customization).
|
||||||
|
|
||||||
|
## Full documentation
|
||||||
|
|
||||||
|
The mapping details live in `references/`: one file per source framework (`mintlify.md`, `docusaurus.md`, `fumadocs.md`, `nextra.md`, `starlight.md`), plus **`monorepo.md`** for host-repo integration (content-layout detection, pnpm `minimumReleaseAge`, frozen-lockfile regeneration, the Vercel monorepo recipe, and the Astro/Vite patch). The Mintlify icon + frontmatter pass is automated by **`scripts/mintlify-codemod.mjs`** (zero-dependency, deterministic, idempotent; `--write` to apply). The authoritative Blume docs are bundled in the installed package's **`docs/` directory** — not necessarily at the repository root: in a workspace monorepo (pnpm especially) the package lives in the depending workspace's `node_modules` (e.g. `apps/docs/node_modules/blume/docs`); `node -e "console.log(require.resolve('blume/package.json'))"` run from the depending package prints the exact location. (In a repo checkout of Blume itself, the docs source is `apps/docs/content/docs`.) The most relevant pages:
|
||||||
|
|
||||||
|
- `configuration/index.mdx` — every `blume.config.ts` field.
|
||||||
|
- `content/navigation.mdx` — the sidebar/tabs/selectors model.
|
||||||
|
- `content/meta.mdx` — `meta.ts` and display modes.
|
||||||
|
- `content/syntax.mdx` — directives, code features, math.
|
||||||
|
- `content/components.mdx` — the component library and APIs.
|
||||||
|
- `content/frontmatter.mdx` — the strict page schema.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
diff --git a/dist/markdown-BMigo7Hm.js b/dist/markdown-BMigo7Hm.js
|
||||||
|
index bc9037f6c0de5516b139d8cdb195b1e25cd33bc0..a02c284e28bb535f9964a8a086ebb6549657e416 100644
|
||||||
|
--- a/dist/markdown-BMigo7Hm.js
|
||||||
|
+++ b/dist/markdown-BMigo7Hm.js
|
||||||
|
@@ -4872,7 +4872,43 @@ function lu(e, t, r) {
|
||||||
|
case "sentence": return Oh(e, r);
|
||||||
|
case "word": return t.parser !== "mdx" ? zh(e, t) : Uh(e);
|
||||||
|
case "whitespace": {
|
||||||
|
- let { next: a } = e, u = a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !NE(e) && !(t.proseWrap === "preserve" && RE(e)) ? "never" : t.proseWrap;
|
||||||
|
+ let { next: a, previous: oxfmtFencePrev } = e;
|
||||||
|
+ // Preserve line breaks that sit directly against a `:::` container
|
||||||
|
+ // directive fence, so `proseWrap: "never"` keeps the opening/closing
|
||||||
|
+ // fence on their own lines instead of joining them into the prose (which
|
||||||
|
+ // breaks the directive). Ordinary prose still wraps per proseWrap.
|
||||||
|
+ // See prettier/prettier#19040.
|
||||||
|
+ let oxfmtIsFence = (w) => w != null && typeof w.value === "string" && w.value.startsWith(":::");
|
||||||
|
+ // A titled directive (`:::warning[Heads up]`) parses its `[title]` as a
|
||||||
|
+ // linkReference between two sentence nodes at the paragraph level: the
|
||||||
|
+ // fence word ends the sentence before the reference, and the body's
|
||||||
|
+ // leading newline opens the sentence after it. So when this whitespace
|
||||||
|
+ // starts its sentence, climb to the paragraph and check whether the two
|
||||||
|
+ // preceding siblings are a (link) reference and a sentence ending in a
|
||||||
|
+ // `:::` fence word.
|
||||||
|
+ let oxfmtPrevIsTitledFence = !1;
|
||||||
|
+ if (oxfmtFencePrev == null && e.index === 0 && e.grandparent != null && Array.isArray(e.grandparent.children)) {
|
||||||
|
+ let oxfmtSibs = e.grandparent.children, oxfmtSentIdx = oxfmtSibs.indexOf(e.parent);
|
||||||
|
+ if (oxfmtSentIdx >= 2) {
|
||||||
|
+ let oxfmtLink = oxfmtSibs[oxfmtSentIdx - 1], oxfmtBefore = oxfmtSibs[oxfmtSentIdx - 2];
|
||||||
|
+ let oxfmtLastWord = oxfmtBefore && oxfmtBefore.type === "sentence" && Array.isArray(oxfmtBefore.children) ? oxfmtBefore.children[oxfmtBefore.children.length - 1] : null;
|
||||||
|
+ oxfmtPrevIsTitledFence = oxfmtLink != null && (oxfmtLink.type === "linkReference" || oxfmtLink.type === "link") && oxfmtIsFence(oxfmtLastWord);
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ // The plain-markdown parser keeps a titled fence's `[title]` as literal
|
||||||
|
+ // words, so the whole directive is one sentence. For a newline
|
||||||
|
+ // whitespace, walk back to the start of its visual line within the
|
||||||
|
+ // sentence; a line led by a `:::` word is a fence whose break must stay.
|
||||||
|
+ if (!oxfmtPrevIsTitledFence && e.node.value.includes("\n") && e.parent != null && Array.isArray(e.parent.children)) {
|
||||||
|
+ let oxfmtLineFirst = null;
|
||||||
|
+ for (let oxfmtJ = e.index - 1; oxfmtJ >= 0; oxfmtJ--) {
|
||||||
|
+ let oxfmtSib = e.parent.children[oxfmtJ];
|
||||||
|
+ if (oxfmtSib.type === "whitespace" && typeof oxfmtSib.value === "string" && oxfmtSib.value.includes("\n")) break;
|
||||||
|
+ oxfmtLineFirst = oxfmtSib;
|
||||||
|
+ }
|
||||||
|
+ oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst);
|
||||||
|
+ }
|
||||||
|
+ let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(a) ? "preserve" : a && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(a.value) && !NE(e) && !(t.proseWrap === "preserve" && RE(e)) ? "never" : t.proseWrap;
|
||||||
|
return ou(e, n.value, u, !1, t);
|
||||||
|
}
|
||||||
|
case "emphasis": {
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Docusaurus → Blume
|
||||||
|
|
||||||
|
Docusaurus 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.
|
||||||
|
|
||||||
|
## Detect
|
||||||
|
|
||||||
|
- **`docusaurus.config.{js,ts}`** at the root; a **`sidebars.{js,ts}`** file and per-folder **`_category_.json`** files.
|
||||||
|
- Content under **`docs/`** (often plus `blog/`, `src/pages/`, `static/`, `versioned_docs/`); `@docusaurus/*` deps.
|
||||||
|
- v2 vs v3: check the `@docusaurus/core` major in `package.json`.
|
||||||
|
|
||||||
|
## Config: `docusaurus.config` → `blume.config.ts`
|
||||||
|
|
||||||
|
Read `themeConfig`, `presets`, and `plugins`:
|
||||||
|
|
||||||
|
| Docusaurus | Blume |
|
||||||
|
| --- | --- |
|
||||||
|
| `title` | `title` |
|
||||||
|
| `tagline` | `description` |
|
||||||
|
| `themeConfig.navbar.title` / `.logo` | `title` / `logo` (move the image into `public/`) |
|
||||||
|
| `themeConfig.navbar.items` (doc items) | `navigation.tabs` (for section links) |
|
||||||
|
| `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) |
|
||||||
|
| `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) |
|
||||||
|
| `themeConfig.colorMode.defaultMode` | `theme.mode` (`respectPrefersColorScheme: true` → `"system"`) |
|
||||||
|
| `themeConfig.prism.theme` / `.darkTheme` | `markdown.code.theme: { light, dark }` (map Prism theme names to Shiki themes, e.g. `github`/`github-dark`) |
|
||||||
|
| `themeConfig.metadata` / `themeConfig.image` | per-page `seo` frontmatter / `seo.og`; report what doesn't fit |
|
||||||
|
| `url` + `baseUrl` | **`url` → `deployment.site`**, unless the target host is Vercel, Netlify, or Cloudflare Pages, 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 |
|
||||||
|
| 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.) |
|
||||||
|
| 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) |
|
||||||
|
| `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`); column `title`s, `copyright`, `logo`, and `style` drop (report), or keep the whole footer as a Footer override (`defineComponents` layout slot) |
|
||||||
|
| `themeConfig.announcementBar` | `banner` (`{ content, dismissible, id }` — `isCloseable` → `dismissible`; colors drop) |
|
||||||
|
| `i18n.locales` / `defaultLocale` | `i18n` — translated files live at `i18n/<locale>/docusaurus-plugin-content-docs/current/…`; move them to `<locale>/…` under `content.root` |
|
||||||
|
| `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) |
|
||||||
|
| `@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 |
|
||||||
|
| `@docusaurus/theme-mermaid` | delete the dep — ` ```mermaid ` renders natively (in `.mdx`) |
|
||||||
|
| 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") |
|
||||||
|
| 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") |
|
||||||
|
| `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 |
|
||||||
|
| Multi-instance docs plugins (`plugin-content-docs` with `id`) | one folder (and usually one `navigation.tabs` entry) per instance |
|
||||||
|
|
||||||
|
## Static assets — required move
|
||||||
|
|
||||||
|
Every 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.
|
||||||
|
|
||||||
|
## Navigation: `sidebars.js` + `_category_.json`
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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).
|
||||||
|
- **`_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).
|
||||||
|
- **`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.
|
||||||
|
|
||||||
|
## Versioned docs
|
||||||
|
|
||||||
|
Docusaurus `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.
|
||||||
|
|
||||||
|
## Blog
|
||||||
|
|
||||||
|
A 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.
|
||||||
|
|
||||||
|
## Content & components
|
||||||
|
|
||||||
|
- **`.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.
|
||||||
|
- **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 silently lost; and v2's `:::warning` rendered **red/danger** — audit whether it should become `:::danger`.
|
||||||
|
- **Tabs:** `<Tabs>`/`<TabItem label="…" value="…">` → `<Tabs>`/`<Tab title="…">`. Drop `groupId`/`queryString`/`value`; strip the `@theme/Tabs` imports.
|
||||||
|
- **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.
|
||||||
|
- **`@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).
|
||||||
|
- **Code blocks:** `title="file.js"` → works as-is; `showLineNumbers` → `lineNumbers`; **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 `.
|
||||||
|
- **MDX v1 (v2 sources) pitfalls:** unescaped `<`/`{` in prose, HTML comments `<!-- -->` (→ `{/* */}`), string `style="…"` attributes (→ objects). Fix as build errors surface.
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
| Docusaurus | Blume |
|
||||||
|
| --- | --- |
|
||||||
|
| `title` / `description` | pass through |
|
||||||
|
| `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 |
|
||||||
|
| `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 |
|
||||||
|
| `sidebar_label` | `sidebar.label` |
|
||||||
|
| `sidebar_position` | `sidebar.order` |
|
||||||
|
| `unlisted` | `hidden: true` + `noindex: true` |
|
||||||
|
| `last_update.date` | `lastModified` |
|
||||||
|
| `draft` | `draft` |
|
||||||
|
| `tags` | `search.tags` (or drop if only used for the blog tag index) |
|
||||||
|
| `image` | `seo.image` |
|
||||||
|
| `keywords` | drop → use `seo` |
|
||||||
|
| `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) |
|
||||||
|
|
||||||
|
Remove any duplicated H1 in the body — `title` renders the H1.
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
Docusaurus 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.
|
||||||
|
|
||||||
|
## Teardown
|
||||||
|
|
||||||
|
Remove `@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`.
|
||||||
|
|
||||||
|
## Dropped — report these
|
||||||
|
|
||||||
|
Custom/swizzled theme components (layout slots or `blume eject`), the footer's copyright, logo, and HTML items, 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.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Fumadocs → Blume
|
||||||
|
|
||||||
|
Fumadocs 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.
|
||||||
|
|
||||||
|
## Detect
|
||||||
|
|
||||||
|
- Content under **`content/docs/`** (the Fumadocs convention).
|
||||||
|
- Per-folder **`meta.json`** files.
|
||||||
|
- `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).
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Fumadocs declares little Blume needs — read the loader and the shared layout options:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
Everything else is Blume defaults.
|
||||||
|
|
||||||
|
**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):
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.
|
||||||
|
- Custom Shiki transformers/themes or other plugins → report.
|
||||||
|
|
||||||
|
## Navigation: `meta.json` → `meta.ts`
|
||||||
|
|
||||||
|
**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`):
|
||||||
|
|
||||||
|
| Fumadocs `meta.json` | Blume `meta.ts` |
|
||||||
|
| --- | --- |
|
||||||
|
| `title` | `title` |
|
||||||
|
| `icon` | `icon` — **convert the casing** (see Icons below) |
|
||||||
|
| `defaultOpen: false` | `collapsed: true` |
|
||||||
|
| `defaultOpen: true` | `collapsed: false` |
|
||||||
|
| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |
|
||||||
|
| `pages: [...]` slugs | `pages: [...]` (ordering) |
|
||||||
|
| `description` | **drop** (folders have no description) |
|
||||||
|
| `collapsible: false` | that folder's `display: "flat"` (a plain heading, even under a global `"group"` mode) |
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
**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"`).
|
||||||
|
|
||||||
|
**`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`.
|
||||||
|
|
||||||
|
Handle the `pages` array items:
|
||||||
|
|
||||||
|
- **`"..."`** (rest marker) / `""` → drop; Blume appends unlisted pages automatically. **`"z...a"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.
|
||||||
|
- **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).
|
||||||
|
- **`"!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.
|
||||||
|
- **`"---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.
|
||||||
|
- **`"...folder"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.
|
||||||
|
- **`"[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`).
|
||||||
|
- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
Fumadocs' 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.
|
||||||
|
|
||||||
|
## Icons: lucide-react names → kebab-case
|
||||||
|
|
||||||
|
Fumadocs 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).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **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]`; drop `icon`.
|
||||||
|
- **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.
|
||||||
|
- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).
|
||||||
|
- **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>`.
|
||||||
|
- **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).
|
||||||
|
- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.
|
||||||
|
- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link: { href, text }, dismissible, id }`).
|
||||||
|
- **`<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.
|
||||||
|
- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Headings
|
||||||
|
|
||||||
|
Trailing 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.
|
||||||
|
|
||||||
|
## Code fences
|
||||||
|
|
||||||
|
- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.
|
||||||
|
- Adjacent fences sharing `tab="…"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.
|
||||||
|
|
||||||
|
## OpenAPI
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
## GraphQL
|
||||||
|
|
||||||
|
`@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.
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
A `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.
|
||||||
|
|
||||||
|
## Package.json & teardown
|
||||||
|
|
||||||
|
Repoint 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).
|
||||||
|
|
||||||
|
## Dropped — report these
|
||||||
|
|
||||||
|
Folder `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.
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# Mintlify → Blume
|
||||||
|
|
||||||
|
The 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.
|
||||||
|
|
||||||
|
## Detect
|
||||||
|
|
||||||
|
- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.
|
||||||
|
- A `mintlify` dependency / `mintlify dev` script.
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
Read `docs.json` first; it drives everything.
|
||||||
|
|
||||||
|
## Config: `docs.json`/`mint.json` → `blume.config.ts`
|
||||||
|
|
||||||
|
Resolve `$ref` includes first (Mintlify splits config across files). Map only what's set:
|
||||||
|
|
||||||
|
| Mintlify | Blume | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `name` / `title` | `title` | |
|
||||||
|
| `description` | `description` | |
|
||||||
|
| `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` |
|
||||||
|
| `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) |
|
||||||
|
| `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) |
|
||||||
|
| `colors.dark` | `theme.action` | |
|
||||||
|
| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) | |
|
||||||
|
| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |
|
||||||
|
| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |
|
||||||
|
| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |
|
||||||
|
| `background.decoration` | **drop** | no Blume equivalent |
|
||||||
|
| `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 |
|
||||||
|
| `banner` | `banner` (`{ content, dismissible, id, link: { href, text } }`) | **only** those keys — drop `banner.color`/`banner.type` |
|
||||||
|
| `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 |
|
||||||
|
| `styling.codeblocks.theme` | `markdown.code.theme` (`{ light, dark }`) | |
|
||||||
|
| `search.prompt` | **drop** | no equivalent |
|
||||||
|
| `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 |
|
||||||
|
| `seo.indexing: "all"` | `search.indexing.includeHiddenPages: true` | |
|
||||||
|
| `variables` (`{{name}}`) | `variables` | same syntax and names (letters, digits, `_`, `-`); pages keep their `{{name}}` references unchanged |
|
||||||
|
| `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` |
|
||||||
|
| `integrations.ga4` (`{ measurementId }`) | `googleAnalytics({ id })` | |
|
||||||
|
| `integrations.gtm` (`{ tagId }`) | `googleTagManager({ id })` | |
|
||||||
|
| `integrations.plausible` (`{ domain, server }`) | `plausible({ domain, host })` | `server` is a bare hostname; `host` is an origin — prepend `https://` |
|
||||||
|
| `integrations.fathom` (`{ siteId }`) | `fathom({ site })` | |
|
||||||
|
| `integrations.pirsch` (`{ id }`) | `pirsch({ code })` | |
|
||||||
|
| `integrations.mixpanel` (`{ projectToken, region }`) | `mixpanel({ token, region })` | |
|
||||||
|
| `integrations.amplitude` (`{ apiKey }`) | `amplitude({ key })` | |
|
||||||
|
| `integrations.segment` (`{ key, cdnUrl }`) | `segment({ key, cdn })` | |
|
||||||
|
| `integrations.hightouch` (`{ writeKey, apiHost }`) | `hightouch({ key, host })` | |
|
||||||
|
| `integrations.heap` (`{ appId }`) | `heap({ id })` | |
|
||||||
|
| `integrations.hotjar` (`{ hjid, hjsv }`) | `hotjar({ id, version })` | both numbers |
|
||||||
|
| `integrations.clarity` (`{ projectId }`) | `clarity({ id })` | |
|
||||||
|
| `integrations.logrocket` (`{ apiKey }`) | `logrocket({ id })` | |
|
||||||
|
| `integrations.clearbit` (`{ publicApiKey }`) | `clearbit({ key })` | |
|
||||||
|
| `integrations.adobe` (`{ launchUrl }`) | `adobe({ url })` | |
|
||||||
|
| any other `integrations` script | `script({ src, strategy, attributes })` / `vercel()` in the `analytics` list | one `script()` adapter per provider without a factory |
|
||||||
|
| `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 |
|
||||||
|
| 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 |
|
||||||
|
| `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 |
|
||||||
|
| 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 |
|
||||||
|
| `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) |
|
||||||
|
| `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) |
|
||||||
|
| `api.mdx` (`{ server, auth: { method, name } }`) | `api: { server, auth: { method, name } }` | the defaults for `api`-frontmatter pages |
|
||||||
|
| `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 |
|
||||||
|
| `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` |
|
||||||
|
| `navigation.languages` | `i18n` | see i18n below |
|
||||||
|
|
||||||
|
A minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.
|
||||||
|
|
||||||
|
## Icons: FontAwesome → Lucide (required)
|
||||||
|
|
||||||
|
Mintlify 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.
|
||||||
|
|
||||||
|
**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 brand/no-equivalent icons, 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):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# <skill> = this skill's directory (the one containing SKILL.md); dry run first (report only), then apply:
|
||||||
|
node <skill>/scripts/mintlify-codemod.mjs <content-dir>
|
||||||
|
node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
The 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:
|
||||||
|
|
||||||
|
| FontAwesome | Lucide | | FontAwesome | Lucide |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `bolt` | `zap` | | `gear`/`cog` | `settings` |
|
||||||
|
| `circle-info`/`info` | `info` | | `wand-magic-sparkles`/`magic` | `sparkles` |
|
||||||
|
| `house` | `house` | | `magnifying-glass` | `search` |
|
||||||
|
| `gauge`/`gauge-high` | `gauge` | | `puzzle-piece` | `puzzle` |
|
||||||
|
| `envelope` | `mail` | | `file-lines` | `file-text` |
|
||||||
|
| `pen-to-square` | `square-pen` | | `trash-can` | `trash-2` |
|
||||||
|
| `xmark`/`times` | `x` | | `circle-check` | `circle-check` |
|
||||||
|
| `triangle-exclamation` | `triangle-alert` | | `circle-exclamation` | `circle-alert` |
|
||||||
|
| `arrow-right-from-bracket` | `log-out` | | `right-to-bracket` | `log-in` |
|
||||||
|
| `location-dot`/`map-marker` | `map-pin` | | `comments` | `messages-square` |
|
||||||
|
| `cube` | `box` | | `cubes`/`boxes` | `boxes` |
|
||||||
|
| `layer-group` | `layers` | | `diagram-project`/`sitemap` | `workflow`/`network` |
|
||||||
|
| `chart-line` | `chart-line` | | `chart-simple`/`chart-column` | `chart-column` |
|
||||||
|
| `flask` | `flask-conical` | | `robot` | `bot` |
|
||||||
|
| `screwdriver-wrench`/`toolbox` | `wrench` | | `circle-question`/`question` | `circle-help` |
|
||||||
|
| `life-ring` | `life-buoy` | | `shield-halved` | `shield` |
|
||||||
|
| `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)_ |
|
||||||
|
|
||||||
|
**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*` — github, discord, x, slack, linkedin…) mostly have **no** Lucide equivalent: for GitHub use the `github` config (renders the footer's repo link); for other socials, use `footer.socials`. Where no Lucide counterpart exists, **drop the icon and report it**. 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.
|
||||||
|
|
||||||
|
## Navigation: `docs.json` `navigation` → filesystem + tabs
|
||||||
|
|
||||||
|
Mintlify'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:
|
||||||
|
|
||||||
|
- **`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.)
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **`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).
|
||||||
|
- **`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:
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.
|
||||||
|
- **`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.)
|
||||||
|
- **`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`.
|
||||||
|
- **`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`.
|
||||||
|
- **`languages`** → `i18n`, not a selector (see below).
|
||||||
|
- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.
|
||||||
|
|
||||||
|
This 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.
|
||||||
|
|
||||||
|
## Content & component transforms
|
||||||
|
|
||||||
|
Rewrite each page's MDX:
|
||||||
|
|
||||||
|
- **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]`. Drop `icon`/color props.
|
||||||
|
- **Accordions — container/item inversion!** Mintlify nests `<Accordion title="…">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title="…">`→`<AccordionItem title="…">` (item).
|
||||||
|
- **`<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.
|
||||||
|
- **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.
|
||||||
|
- **Multi-word code-block titles need quotes.** Mintlify reads everything after the language as the title (` ```ts Acme SDK `); Blume reads one bare word, so write ` ```ts title="Acme SDK" `. One-word titles (` ```bash cURL `) stay as they are.
|
||||||
|
- **`<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.
|
||||||
|
- **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.
|
||||||
|
- **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>`.
|
||||||
|
- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `githubReleases()` source.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
Mintlify 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:
|
||||||
|
|
||||||
|
| Mintlify | Blume | Codemod |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `title` / `description` | pass through | — |
|
||||||
|
| `sidebarTitle` | `sidebar.label` | renames |
|
||||||
|
| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |
|
||||||
|
| `tag` | `sidebar.badge` | renames |
|
||||||
|
| `canonical` | `seo.canonical` | renames |
|
||||||
|
| `og:image` | `seo.image` | renames |
|
||||||
|
| `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) |
|
||||||
|
| `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 |
|
||||||
|
| `api`, `authMethod`, `playground` | kept as written (a hand-written endpoint page, see above) | left |
|
||||||
|
| `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) |
|
||||||
|
| `mode` | `mode`, as written | `default`, `wide`, `center`, `custom`, and `frame` carry over; `assistant` has no equivalent → drop (report) |
|
||||||
|
| `hideFooterPagination: true` | `pagination: false` | renames (`false` drops, since the links show by default) |
|
||||||
|
| `keywords`, `boost` | `search.keywords`, `search.boost` | renames |
|
||||||
|
| `searchable: false` | `search.exclude: true` | renames (`searchable: true` drops, since pages are searchable by default) |
|
||||||
|
| `public`, `rss`, `groups`, `hideApiMarker`, `iconType` | **drop** (report) | drops |
|
||||||
|
|
||||||
|
The 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.)
|
||||||
|
|
||||||
|
## OpenAPI
|
||||||
|
|
||||||
|
Top-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.
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/list-models`: a camelCase 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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
Mintlify 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.
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
Per-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.
|
||||||
|
|
||||||
|
## Dropped — report these
|
||||||
|
|
||||||
|
- **A group's `boost`** (on a `navigation` group, inherited by its pages) → no inherited boost; set `search.boost` on the pages that need it.
|
||||||
|
- **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.
|
||||||
|
- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`.
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Migrating into a real repo (monorepos, pnpm, Vercel)
|
||||||
|
|
||||||
|
The 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Non-standard content layout → scope `content.root` + `include`
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# top two path segments of every content file, deduped — your content folders
|
||||||
|
find . -path ./node_modules -prune -o \( -name '*.md' -o -name '*.mdx' \) -print \
|
||||||
|
| sed 's|^\./||' | cut -d/ -f1 | sort -u
|
||||||
|
```
|
||||||
|
|
||||||
|
If 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.
|
||||||
|
|
||||||
|
**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`:
|
||||||
|
|
||||||
|
```ts blume.config.ts
|
||||||
|
import { defineConfig } from "blume";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
content: {
|
||||||
|
root: ".", // content sits directly under this package (apps/docs/), no docs/ subfolder
|
||||||
|
// include/exclude globs are relative to `content.root`. List the actual
|
||||||
|
// content folders so the scan never wanders into src/, public/, etc.
|
||||||
|
include: [
|
||||||
|
"api/**/*.{md,mdx}",
|
||||||
|
"getting-started/**/*.{md,mdx}",
|
||||||
|
"guides/**/*.{md,mdx}",
|
||||||
|
],
|
||||||
|
exclude: ["**/node_modules/**", "**/_*", "**/.*"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Schema 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`, default `["**/_*", "**/.*"]`). Singular `include`/`exclude`, both arrays.
|
||||||
|
|
||||||
|
Report 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. pnpm `minimumReleaseAge` blocks installing fresh Blume
|
||||||
|
|
||||||
|
pnpm'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.
|
||||||
|
|
||||||
|
**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.
|
||||||
|
|
||||||
|
**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "minimumReleaseAge\|minimum-release-age" pnpm-workspace.yaml .npmrc 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
minimumReleaseAge: 1440
|
||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- blume
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix — `.npmrc`** form (if the policy lives there instead):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
minimum-release-age=1440
|
||||||
|
minimum-release-age-exclude[]=blume
|
||||||
|
```
|
||||||
|
|
||||||
|
Report: "Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Frozen lockfile — regenerate and commit `pnpm-lock.yaml` in the same change
|
||||||
|
|
||||||
|
CI 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.
|
||||||
|
|
||||||
|
**After every dependency edit, regenerate the lockfile and stage it with the manifest:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml
|
||||||
|
git add package.json apps/docs/package.json pnpm-lock.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Vercel monorepo recipe (root-aware install + build)
|
||||||
|
|
||||||
|
A 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/`. `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.
|
||||||
|
|
||||||
|
This is a **copyable template**, not prose. Three pieces:
|
||||||
|
|
||||||
|
**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "blume dev",
|
||||||
|
"build": "blume build",
|
||||||
|
"preview": "blume preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"blume": "^2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"framework": null,
|
||||||
|
"installCommand": "cd ../.. && pnpm install --frozen-lockfile",
|
||||||
|
"buildCommand": "pnpm run build",
|
||||||
|
"outputDirectory": "dist"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**
|
||||||
|
|
||||||
|
- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.
|
||||||
|
- **Node version** = 22 or newer (Blume requires it). Or pin it in the docs package: `"engines": { "node": ">=22" }`.
|
||||||
|
|
||||||
|
How the pieces fit, with Root Directory = `apps/docs`:
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.
|
||||||
|
- `outputDirectory: "dist"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
If 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`.
|
||||||
|
|
||||||
|
Report every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.
|
||||||
|
|
||||||
|
**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.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Astro/Vite version mismatch → pnpm patch (manual)
|
||||||
|
|
||||||
|
Blume 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.
|
||||||
|
|
||||||
|
**Detect the risk factor** before it bites:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "\"vite\"\|vite@\| vite:" pnpm-workspace.yaml package.json 2>/dev/null | grep -i "override\|resolution\|catalog\|vite"
|
||||||
|
```
|
||||||
|
|
||||||
|
Blume 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):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Open the offending version in an editable temp dir (use the exact version
|
||||||
|
# pnpm resolved — check `pnpm why astro`):
|
||||||
|
pnpm patch astro@7.0.6
|
||||||
|
|
||||||
|
# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface
|
||||||
|
# (the exact edit depends on the mismatch; the crash's stack trace names the
|
||||||
|
# file and call site). Then commit the patch:
|
||||||
|
pnpm patch-commit '<the temp path pnpm printed>'
|
||||||
|
```
|
||||||
|
|
||||||
|
`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):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# pnpm-workspace.yaml
|
||||||
|
patchedDependencies:
|
||||||
|
astro@7.0.6: patches/astro@7.0.6.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.
|
||||||
|
|
||||||
|
This 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)
|
||||||
|
|
||||||
|
If 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`.
|
||||||
|
|
||||||
|
The fix is a committed **pnpm patch** (`patches/oxfmt@0.67.0.patch`), shipped with this skill at `assets/oxfmt@0.67.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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;
|
||||||
|
# <skill> = this skill's directory, the one containing SKILL.md):
|
||||||
|
mkdir -p patches
|
||||||
|
cp "<skill>/assets/oxfmt@0.67.0.patch" patches/oxfmt@0.67.0.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# pnpm-workspace.yaml
|
||||||
|
patchedDependencies:
|
||||||
|
oxfmt@0.67.0: patches/oxfmt@0.67.0.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.
|
||||||
|
|
||||||
|
- **The patch is pinned to `oxfmt@0.67.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.67.0` as a direct dev dependency (Ultracite accepts any `oxfmt` ≥ 0.40 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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist for a workspace migration
|
||||||
|
|
||||||
|
- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.
|
||||||
|
- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.
|
||||||
|
- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.
|
||||||
|
- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22 in the Vercel project.
|
||||||
|
- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.
|
||||||
|
- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.67.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.
|
||||||
|
- [ ] Reported every repo-specific edit and every manual step left to the user.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Nextra → Blume
|
||||||
|
|
||||||
|
Nextra (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.
|
||||||
|
|
||||||
|
## Detect & fingerprint the generation
|
||||||
|
|
||||||
|
- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).
|
||||||
|
- **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).
|
||||||
|
- **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`**.
|
||||||
|
- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
None of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:
|
||||||
|
|
||||||
|
- **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` → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.
|
||||||
|
- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>`. Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).
|
||||||
|
- **`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.
|
||||||
|
- Root `_meta` entries with **`type: "page"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: "page"` only maps at the **root**.
|
||||||
|
|
||||||
|
## Navigation: `_meta` → `meta.ts` + frontmatter
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
For each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):
|
||||||
|
|
||||||
|
| Nextra `_meta` entry | Blume |
|
||||||
|
| --- | --- |
|
||||||
|
| 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. |
|
||||||
|
| `display: "hidden"` | frontmatter `sidebar.hidden: true` |
|
||||||
|
| `type: "separator"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |
|
||||||
|
| `type: "menu"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |
|
||||||
|
| `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) |
|
||||||
|
| `type: "page"` (subfolder, not root) | drop (only root → tabs) |
|
||||||
|
| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |
|
||||||
|
| `theme: { layout: "full" }` on a page | that page's frontmatter `mode: wide` (full column width, no table of contents — the closest match; report) |
|
||||||
|
| other `theme` keys (`sidebar: false`, `toc: false`, `typesetting`, …) | drop (report — no per-page switches for them) |
|
||||||
|
| `"*"` wildcard entry | apply its value as the default for unlisted siblings, then drop |
|
||||||
|
|
||||||
|
Write 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Frontmatter & titles — synthesize, don't just pass through
|
||||||
|
|
||||||
|
Most 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.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **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).
|
||||||
|
- **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`.
|
||||||
|
- **`<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.
|
||||||
|
- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).
|
||||||
|
- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.
|
||||||
|
- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` → `:::note`/`:::tip`/`:::warning`/`:::note`/`:::warning` directives — Blume renders them as plain blockquotes otherwise.
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
## Code fences
|
||||||
|
|
||||||
|
Nextra's fence meta differs from Blume's — rewrite it: `filename="app.js"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers`; 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 `.
|
||||||
|
|
||||||
|
## Math
|
||||||
|
|
||||||
|
Nextra 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.
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Package.json & teardown
|
||||||
|
|
||||||
|
Repoint `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).
|
||||||
|
|
||||||
|
## Dropped — report these
|
||||||
|
|
||||||
|
Footer content; `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.
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Starlight → Blume
|
||||||
|
|
||||||
|
Starlight 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).
|
||||||
|
|
||||||
|
## Detect
|
||||||
|
|
||||||
|
- `astro.config.{mjs,mts,ts,js,cjs}` importing and calling **`starlight({…})`** (`import starlight from "@astrojs/starlight"`).
|
||||||
|
- Content under **`src/content/docs/`**; a `src/content.config.ts` (current) or `src/content/config.ts` (pre-Astro-5).
|
||||||
|
- `@astrojs/starlight` dep.
|
||||||
|
|
||||||
|
Keep content where it is — set `content.root: "src/content/docs"`.
|
||||||
|
|
||||||
|
## Config: `starlight({…})` → `blume.config.ts`
|
||||||
|
|
||||||
|
**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, Netlify, or Cloudflare Pages, which Blume auto-detects (see SKILL.md); other integrations → report.
|
||||||
|
|
||||||
|
| Starlight option | Blume |
|
||||||
|
| --- | --- |
|
||||||
|
| `title` | `title` (if per-locale/computed, set manually) |
|
||||||
|
| `description` | `description` |
|
||||||
|
| `logo` (`{ src }` or `{ light, dark, alt }`) | `logo` — move the referenced file into `public/` |
|
||||||
|
| `logo.replacesTitle` | `logo: { text: "" }` (renders the mark alone) |
|
||||||
|
| `favicon` | copy the file into `public/` (drop the config field — Blume auto-detects) |
|
||||||
|
| `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) |
|
||||||
|
| `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) |
|
||||||
|
| `sidebar` (array) | filesystem nav / `navigation.sidebar` (see below) |
|
||||||
|
| `tableOfContents` (`false` or `{ minHeadingLevel, maxHeadingLevel }`) | `toc` — identical shape, 1:1 |
|
||||||
|
| `markdown.headingLinks: false` | `markdown.headingAnchors: false` |
|
||||||
|
| `expressiveCode.themes` | `markdown.code.theme: { light, dark }` — EC assigns by theme _type_, so match each theme by its darkness, not by array position |
|
||||||
|
| `expressiveCode.styleOverrides` | drop → restyle via a root `theme.css` file |
|
||||||
|
| `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 |
|
||||||
|
| `lastUpdated: true` | `lastModified: "git"` |
|
||||||
|
| `customCss` | drop → move into a root **`theme.css`** file (auto-picked-up; not a config field) |
|
||||||
|
| `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 |
|
||||||
|
| `plugins` | **map, don't blanket-drop** — see Plugins below |
|
||||||
|
| `pagination: false`, `pagefind`/Pagefind options, `titleDelimiter`, `credits`, `disable404Route`, `routeMiddleware` | drop (report) |
|
||||||
|
| `locales` / `defaultLocale` | `i18n` (see below) |
|
||||||
|
|
||||||
|
## Navigation: `sidebar`
|
||||||
|
|
||||||
|
Starlight's `sidebar` array → prefer letting Blume generate from the filesystem; use `navigation.sidebar` only for shapes files can't express:
|
||||||
|
|
||||||
|
- `{ 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.
|
||||||
|
- string `"guides/intro"` → `"/guides/intro"`; `{ label?, slug }` → `/<slug>`.
|
||||||
|
- `{ label, items: [...] }` → `{ label, items: [...] }` (recursive) — or better, a real folder.
|
||||||
|
- `{ label?, link }` → `{ label, href: link }`.
|
||||||
|
- `badge` (string or `{ text, variant }`) → `badge` (text only; variant drops). `collapsed` (bool) → `collapsed`. Item `attrs` and `translations` → drop (report).
|
||||||
|
|
||||||
|
## Frontmatter
|
||||||
|
|
||||||
|
| Starlight | Blume |
|
||||||
|
| --- | --- |
|
||||||
|
| `title` / `description` / `draft` / `slug` | pass through |
|
||||||
|
| `sidebar.{label,order,hidden}` | pass through |
|
||||||
|
| `sidebar.badge` | `sidebar.badge` (text only) |
|
||||||
|
| `sidebar.attrs` | **remove** (strict schema — build error if left) |
|
||||||
|
| `pagefind: false` | `search.exclude: true` |
|
||||||
|
| `lastUpdated` (date/string) | `lastModified` |
|
||||||
|
| `lastUpdated: false` (boolean) | **remove** (Blume's `lastModified` takes a date; a boolean is a build error) |
|
||||||
|
| `prev: false` **and** `next: false` | `pagination: false` (hides both links) |
|
||||||
|
| `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) |
|
||||||
|
| `template: splash` | `mode: center` (no sidebar, a wider centered column — the closest layout; report) |
|
||||||
|
| `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) |
|
||||||
|
| `banner`, `tableOfContents`, `editUrl`, `head` | drop (report) |
|
||||||
|
|
||||||
|
## Asides → directives (and the `.md` trap)
|
||||||
|
|
||||||
|
Starlight'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. 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.
|
||||||
|
|
||||||
|
- `<Aside type="…" title="…">` (the component form) → the same directives; bare `<Aside>` → `:::note`.
|
||||||
|
- An aside custom icon (`:::tip{icon="heart"}`) → drop the attr (report).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **`<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`).
|
||||||
|
- **Convert yourself:** `<Steps>` → Blume `<Steps>`/`<Step>`; `<FileTree>` → Blume `<FileTree>`; `<Code code={…}>` → a fenced code block; `<LinkButton>` → a Markdown link or `<Card>`.
|
||||||
|
- Strip `import … from "@astrojs/starlight/*"` and `astro:assets` lines.
|
||||||
|
|
||||||
|
## Code blocks: Expressive Code meta → Blume
|
||||||
|
|
||||||
|
Starlight 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:
|
||||||
|
|
||||||
|
- `title="file.js"` → works as-is (or use the space-title shorthand). Line ranges `{2-3}` → work as-is.
|
||||||
|
- `ins=`/`del=` line marks → `// [!code ++]` / `// [!code --]` comments; `mark=` → `{ranges}` or `// [!code highlight]`.
|
||||||
|
- `showLineNumbers` → `lineNumbers`.
|
||||||
|
- `wrap` / `wrap=true` → a bare `wrap` (that block's long lines wrap); a site-wide `expressiveCode.defaultProps.wrap: true` → `markdown.code.wrap: true`.
|
||||||
|
- **Drop:** `frame="terminal"`, `collapse=`, `wrap=false`, `preserveIndent`/`hangingIndent`, `"string"` and `/regex/` text markers (report if they carried meaning).
|
||||||
|
- ` ```diff lang="js" ` → a normal ` ```js ` fence with `[!code ++]`/`[!code --]` markers.
|
||||||
|
|
||||||
|
## Plugins — map, don't drop
|
||||||
|
|
||||||
|
- `starlight-openapi` → an `openapi({ sources })` entry in Blume's `reference` list, imported from `blume/reference` (delete any generated pages; add the `navigation.tabs` entry).
|
||||||
|
- `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.
|
||||||
|
- `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.
|
||||||
|
- `starlight-image-zoom` → delete (Blume zooms content images by default).
|
||||||
|
- `starlight-links-validator` → delete (`blume validate` covers it).
|
||||||
|
- Anything else → report.
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
Starlight 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>`).
|
||||||
|
|
||||||
|
## i18n
|
||||||
|
|
||||||
|
Starlight has **two** layouts:
|
||||||
|
|
||||||
|
- **`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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
Don't restate Blume defaults (`hideDefaultLocalePrefix: true`, `parser: "dir"` are already the defaults). Starlight's untranslated-page fallback matches Blume's `fallbackLocale` default.
|
||||||
|
|
||||||
|
## Teardown
|
||||||
|
|
||||||
|
Remove `@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).
|
||||||
|
|
||||||
|
## Dropped — report these
|
||||||
|
|
||||||
|
Socials 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.
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// mintlify-codemod.mjs — deterministic, idempotent frontmatter codemod for the
|
||||||
|
// Mintlify → Blume migration. It rewrites ONLY the YAML frontmatter block of
|
||||||
|
// `.md`/`.mdx` files; the body is never touched. Two passes:
|
||||||
|
//
|
||||||
|
// 1. Icons — remap Mintlify (FontAwesome) `icon:` names to their closest
|
||||||
|
// Lucide equivalent (Blume is Lucide-only). Brand/no-equivalent
|
||||||
|
// icons are dropped and reported, never faked.
|
||||||
|
// 2. Fields — drop frontmatter keys Blume's strict schema rejects, and rename
|
||||||
|
// Mintlify-only keys to their Blume nesting (sidebarTitle →
|
||||||
|
// sidebar.label, tag → sidebar.badge, canonical → seo.canonical,
|
||||||
|
// og:image → seo.image). Ambiguous keys (openapi/asyncapi/api)
|
||||||
|
// are flagged for human review, not transformed.
|
||||||
|
//
|
||||||
|
// Design constraints:
|
||||||
|
// - ZERO dependencies — safe in a pnpm-strict workspace with no hoisting; runs
|
||||||
|
// with a bare `node`, needs nothing from the target repo's node_modules.
|
||||||
|
// - Deterministic — no Date/Math.random; files processed in sorted order.
|
||||||
|
// - Idempotent — running twice makes no further change. Source keys are gone
|
||||||
|
// after the first pass; remapped icon values are already Lucide (every
|
||||||
|
// mapped value that is also a source key maps to itself); a rename whose
|
||||||
|
// target already exists is reported, not re-applied.
|
||||||
|
// - Surgical — untouched lines keep their exact formatting; only the specific
|
||||||
|
// lines that change are edited, so diffs stay small and reviewable.
|
||||||
|
// - Reports every change per file (and every drop/flag), so nothing is silent.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node mintlify-codemod.mjs <path...> # dry run — report only
|
||||||
|
// node mintlify-codemod.mjs --write <path...> # apply changes in place
|
||||||
|
// node mintlify-codemod.mjs --json <path...> # machine-readable report
|
||||||
|
// node mintlify-codemod.mjs --help
|
||||||
|
//
|
||||||
|
// Paths may be files or directories; directories are walked for .md/.mdx.
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// --- Mintlify (FontAwesome) → Lucide icon map ------------------------------
|
||||||
|
// Keys are lowercased Mintlify icon names; values are Lucide names. A value of
|
||||||
|
// `null` means "no Lucide equivalent" (brand icons, mostly) — the icon line is
|
||||||
|
// removed and reported. Every value that is itself a key maps to the same name,
|
||||||
|
// which keeps the pass idempotent (a second run finds nothing to change). The
|
||||||
|
// FontAwesome `x` (close) maps to Lucide `x`; the X/Twitter brand is
|
||||||
|
// `x-twitter` and has no Lucide form.
|
||||||
|
const ICONS = {
|
||||||
|
angular: null,
|
||||||
|
apple: null,
|
||||||
|
"arrow-right-from-bracket": "log-out",
|
||||||
|
aws: null,
|
||||||
|
bell: "bell",
|
||||||
|
bolt: "zap",
|
||||||
|
book: "book",
|
||||||
|
"book-open": "book-open",
|
||||||
|
boxes: "boxes",
|
||||||
|
calendar: "calendar",
|
||||||
|
"chart-column": "chart-column",
|
||||||
|
"chart-line": "chart-line",
|
||||||
|
"chart-simple": "chart-column",
|
||||||
|
check: "check",
|
||||||
|
"circle-check": "circle-check",
|
||||||
|
"circle-exclamation": "circle-alert",
|
||||||
|
"circle-info": "info",
|
||||||
|
"circle-question": "circle-help",
|
||||||
|
cloud: "cloud",
|
||||||
|
code: "code",
|
||||||
|
cog: "settings",
|
||||||
|
comments: "messages-square",
|
||||||
|
copy: "copy",
|
||||||
|
cube: "box",
|
||||||
|
cubes: "boxes",
|
||||||
|
database: "database",
|
||||||
|
"diagram-project": "workflow",
|
||||||
|
discord: null,
|
||||||
|
docker: null,
|
||||||
|
download: "download",
|
||||||
|
envelope: "mail",
|
||||||
|
facebook: null,
|
||||||
|
"file-lines": "file-text",
|
||||||
|
filter: "filter",
|
||||||
|
flask: "flask-conical",
|
||||||
|
folder: "folder",
|
||||||
|
gauge: "gauge",
|
||||||
|
"gauge-high": "gauge",
|
||||||
|
gear: "settings",
|
||||||
|
github: null,
|
||||||
|
gitlab: null,
|
||||||
|
globe: "globe",
|
||||||
|
google: null,
|
||||||
|
heart: "heart",
|
||||||
|
house: "house",
|
||||||
|
info: "info",
|
||||||
|
instagram: null,
|
||||||
|
java: null,
|
||||||
|
js: null,
|
||||||
|
key: "key",
|
||||||
|
"layer-group": "layers",
|
||||||
|
layers: "layers",
|
||||||
|
"life-ring": "life-buoy",
|
||||||
|
link: "link",
|
||||||
|
linkedin: null,
|
||||||
|
"location-dot": "map-pin",
|
||||||
|
lock: "lock",
|
||||||
|
magic: "sparkles",
|
||||||
|
"magnifying-glass": "search",
|
||||||
|
"map-marker": "map-pin",
|
||||||
|
medium: null,
|
||||||
|
microsoft: null,
|
||||||
|
node: null,
|
||||||
|
"node-js": null,
|
||||||
|
npm: null,
|
||||||
|
"pen-to-square": "square-pen",
|
||||||
|
php: null,
|
||||||
|
play: "play",
|
||||||
|
puzzle: "puzzle",
|
||||||
|
"puzzle-piece": "puzzle",
|
||||||
|
python: null,
|
||||||
|
question: "circle-help",
|
||||||
|
react: null,
|
||||||
|
"right-to-bracket": "log-in",
|
||||||
|
robot: "bot",
|
||||||
|
rocket: "rocket",
|
||||||
|
rust: null,
|
||||||
|
"screwdriver-wrench": "wrench",
|
||||||
|
search: "search",
|
||||||
|
server: "server",
|
||||||
|
settings: "settings",
|
||||||
|
shield: "shield",
|
||||||
|
"shield-halved": "shield",
|
||||||
|
sitemap: "network",
|
||||||
|
slack: null,
|
||||||
|
sparkles: "sparkles",
|
||||||
|
star: "star",
|
||||||
|
stripe: null,
|
||||||
|
tag: "tag",
|
||||||
|
telegram: null,
|
||||||
|
terminal: "terminal",
|
||||||
|
times: "x",
|
||||||
|
toolbox: "wrench",
|
||||||
|
"trash-can": "trash-2",
|
||||||
|
"triangle-exclamation": "triangle-alert",
|
||||||
|
twitter: null,
|
||||||
|
upload: "upload",
|
||||||
|
user: "user",
|
||||||
|
users: "users",
|
||||||
|
vuejs: null,
|
||||||
|
"wand-magic-sparkles": "sparkles",
|
||||||
|
whatsapp: null,
|
||||||
|
workflow: "workflow",
|
||||||
|
wrench: "wrench",
|
||||||
|
x: "x",
|
||||||
|
"x-twitter": null,
|
||||||
|
xmark: "x",
|
||||||
|
youtube: null,
|
||||||
|
zap: "zap",
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Frontmatter field policy ----------------------------------------------
|
||||||
|
// Top-level keys Blume's strict schema rejects: delete the whole block, report.
|
||||||
|
const DROP = new Set(["groups", "hideApiMarker", "iconType", "public", "rss"]);
|
||||||
|
|
||||||
|
// Keys whose fate depends on their value: the source value (and its line) →
|
||||||
|
// the line that replaces it, the line itself to keep it, or `null` to drop it
|
||||||
|
// (a value Blume has no counterpart for, or one that restates its default).
|
||||||
|
const REPLACE = {
|
||||||
|
hideFooterPagination: (value) =>
|
||||||
|
value === "true" ? "pagination: false" : null,
|
||||||
|
mode: (value, line) => (value === "assistant" ? null : line),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mintlify-only keys → Blume nested target. `[parent, child]`, plus a value
|
||||||
|
// mapper where the meaning flips: it returns the new value, or `null` to drop a
|
||||||
|
// value that restates Blume's default.
|
||||||
|
const RENAME = {
|
||||||
|
boost: ["search", "boost"],
|
||||||
|
canonical: ["seo", "canonical"],
|
||||||
|
keywords: ["search", "keywords"],
|
||||||
|
"og:image": ["seo", "image"],
|
||||||
|
ogImage: ["seo", "image"],
|
||||||
|
searchable: [
|
||||||
|
"search",
|
||||||
|
"exclude",
|
||||||
|
(value) => (value === "false" ? "true" : null),
|
||||||
|
],
|
||||||
|
sidebarTitle: ["sidebar", "label"],
|
||||||
|
tag: ["sidebar", "badge"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keys we deliberately do NOT auto-transform — they usually mean the page is an
|
||||||
|
// OpenAPI endpoint stub that should be deleted (Blume generates operation pages),
|
||||||
|
// or else a normal page that just loses the key. Flag for the human; never guess.
|
||||||
|
// (`api` is not one: a hand-written endpoint page keeps it as written.)
|
||||||
|
const FLAG = new Set(["asyncapi", "openapi"]);
|
||||||
|
|
||||||
|
// Which change kinds actually edit the file. Report-only kinds (flags,
|
||||||
|
// unknowns, conflicts, manual-rename notices) leave the bytes untouched.
|
||||||
|
const MUTATING = new Set(["drop", "icon-drop", "icon-remap", "rename"]);
|
||||||
|
|
||||||
|
// --- Frontmatter line model -------------------------------------------------
|
||||||
|
|
||||||
|
// Split a file into { fm, body, eol } or null when there's no `---` block.
|
||||||
|
const splitFrontmatter = (text) => {
|
||||||
|
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
||||||
|
const lines = text.split(/\r?\n/u);
|
||||||
|
if (lines[0] !== "---") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let end = -1;
|
||||||
|
for (let i = 1; i < lines.length; i += 1) {
|
||||||
|
if (lines[i] === "---") {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (end === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { body: lines.slice(end + 1), eol, fm: lines.slice(1, end) };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Match a top-level (column-0) `key:` line, honoring quoted keys. Because the
|
||||||
|
// key class allows `:`, backtracking resolves `og:image: x` to key `og:image`.
|
||||||
|
const topKey = (line) => {
|
||||||
|
const m = /^(?<q>["']?)(?<key>[^"'\s:][^"']*?)\k<q>\s*:(?<rest>\s.*|)$/u.exec(
|
||||||
|
line
|
||||||
|
);
|
||||||
|
if (!m || /^\s/u.test(line)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { key: m.groups.key, value: (m.groups.rest ?? "").trim() };
|
||||||
|
};
|
||||||
|
|
||||||
|
// The line index range [start, endExclusive) of a top-level key's block: its
|
||||||
|
// indented lines plus any `- ` sequence items written flush at column 0,
|
||||||
|
// which YAML also reads as the key's value (`keywords:` then `- one`).
|
||||||
|
const blockRange = (fm, start) => {
|
||||||
|
let end = start + 1;
|
||||||
|
while (
|
||||||
|
end < fm.length &&
|
||||||
|
(fm[end] === "" || /^\s/u.test(fm[end]) || /^-(?:\s|$)/u.test(fm[end]))
|
||||||
|
) {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
// Trim trailing blank lines back out so the gap before the next key survives.
|
||||||
|
while (end - 1 > start && fm[end - 1] === "") {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
return [start, end];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Does the block starting at `start` carry a nested child `childKey:`?
|
||||||
|
const findChild = (fm, start, endExclusive, childKey) => {
|
||||||
|
for (let i = start + 1; i < endExclusive; i += 1) {
|
||||||
|
const m = /^\s+(?<q>["']?)(?<key>[^"'\s:][^"']*?)\k<q>\s*:/u.exec(fm[i]);
|
||||||
|
if (m && m.groups.key === childKey) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- The two passes ---------------------------------------------------------
|
||||||
|
|
||||||
|
// Remap `icon:` values in place. Mutates `fm`; returns change records.
|
||||||
|
const remapIcons = (fm) => {
|
||||||
|
const changes = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < fm.length) {
|
||||||
|
const m =
|
||||||
|
/^(?<indent>\s*)icon:\s*(?<q>["']?)(?<name>[^"'#]*?)\k<q>\s*(?<comment>#.*)?$/u.exec(
|
||||||
|
fm[i]
|
||||||
|
);
|
||||||
|
if (!m) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const raw = m.groups.name.trim();
|
||||||
|
// Empty, an object, or a template ref — leave it for the human.
|
||||||
|
if (raw === "" || raw.startsWith("{") || raw.startsWith("$")) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = raw.toLowerCase();
|
||||||
|
if (!(key in ICONS)) {
|
||||||
|
changes.push({ detail: raw, kind: "icon-unknown" });
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lucide = ICONS[key];
|
||||||
|
if (lucide === null) {
|
||||||
|
changes.push({ detail: raw, kind: "icon-drop" });
|
||||||
|
fm.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lucide !== raw) {
|
||||||
|
const comment = m.groups.comment ? ` ${m.groups.comment}` : "";
|
||||||
|
fm[i] = `${m.groups.indent}icon: ${lucide}${comment}`;
|
||||||
|
changes.push({ detail: `${raw} → ${lucide}`, kind: "icon-remap" });
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert `child: value` under `parent`, creating the parent block if needed.
|
||||||
|
const setNested = (fm, parent, child, value) => {
|
||||||
|
for (let i = 0; i < fm.length; i += 1) {
|
||||||
|
const tk = topKey(fm[i]);
|
||||||
|
if (!tk || tk.key !== parent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tk.value !== "") {
|
||||||
|
// `parent: scalar` — can't nest under a scalar without clobbering it.
|
||||||
|
return { ok: false, reason: "parent-is-scalar" };
|
||||||
|
}
|
||||||
|
const [start, end] = blockRange(fm, i);
|
||||||
|
if (findChild(fm, start, end, child) !== -1) {
|
||||||
|
return { ok: false, reason: "child-exists" };
|
||||||
|
}
|
||||||
|
fm.splice(start + 1, 0, ` ${child}: ${value}`);
|
||||||
|
return { index: start + 1, ok: true };
|
||||||
|
}
|
||||||
|
// No parent block — append one at the end of the frontmatter.
|
||||||
|
const index = fm.length;
|
||||||
|
fm.push(`${parent}:`, ` ${child}: ${value}`);
|
||||||
|
return { index, ok: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drop unsupported keys, rename Mintlify-only keys, flag ambiguous ones.
|
||||||
|
const rewriteFields = (fm) => {
|
||||||
|
const changes = [];
|
||||||
|
// Walk from the bottom so splices don't shift indices we haven't visited.
|
||||||
|
for (let i = fm.length - 1; i >= 0; i -= 1) {
|
||||||
|
const tk = topKey(fm[i]);
|
||||||
|
if (!tk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (DROP.has(tk.key)) {
|
||||||
|
const [start, end] = blockRange(fm, i);
|
||||||
|
fm.splice(start, end - start);
|
||||||
|
changes.push({ detail: tk.key, kind: "drop" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (FLAG.has(tk.key)) {
|
||||||
|
changes.push({ detail: tk.key, kind: "flag" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(REPLACE, tk.key)) {
|
||||||
|
const [start, end] = blockRange(fm, i);
|
||||||
|
const value = tk.value.replace(
|
||||||
|
/^(?<q>["'])(?<inner>.*)\k<q>$/u,
|
||||||
|
"$<inner>"
|
||||||
|
);
|
||||||
|
const line =
|
||||||
|
end - start === 1 ? REPLACE[tk.key](value, fm[start]) : undefined;
|
||||||
|
if (line === undefined) {
|
||||||
|
changes.push({ detail: tk.key, kind: "rename-manual" });
|
||||||
|
} else if (line === null) {
|
||||||
|
fm.splice(start, 1);
|
||||||
|
changes.push({ detail: `${tk.key}: ${value}`, kind: "drop" });
|
||||||
|
} else if (line !== fm[start]) {
|
||||||
|
const [key] = line.split(":");
|
||||||
|
if (fm.some((other, j) => j !== start && topKey(other)?.key === key)) {
|
||||||
|
changes.push({
|
||||||
|
detail: `${tk.key} → ${key} (child-exists)`,
|
||||||
|
kind: "rename-conflict",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fm[start] = line;
|
||||||
|
changes.push({
|
||||||
|
detail: `${tk.key}: ${value} → ${line}`,
|
||||||
|
kind: "rename",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = RENAME[tk.key];
|
||||||
|
if (!target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [parent, child, map] = target;
|
||||||
|
const [start, end] = blockRange(fm, i);
|
||||||
|
if (end - start !== 1 || tk.value === "") {
|
||||||
|
// Multi-line or valueless source — too structured to move safely.
|
||||||
|
changes.push({
|
||||||
|
detail: `${tk.key} → ${parent}.${child}`,
|
||||||
|
kind: "rename-manual",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = map ? map(tk.value) : tk.value;
|
||||||
|
if (value === null) {
|
||||||
|
fm.splice(start, 1);
|
||||||
|
changes.push({ detail: `${tk.key}: ${tk.value}`, kind: "drop" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Remove the source line before inserting: setNested splices into the
|
||||||
|
// parent block, and when that block sits above the source key the insert
|
||||||
|
// would otherwise shift `start` onto the wrong line. Failure paths don't
|
||||||
|
// mutate `fm`, so the line can be restored as-is on conflict.
|
||||||
|
const [removed] = fm.splice(start, 1);
|
||||||
|
const placed = setNested(fm, parent, child, value);
|
||||||
|
if (placed.ok) {
|
||||||
|
if (placed.index < start) {
|
||||||
|
// The insert above the cursor pushed the unvisited lines down one
|
||||||
|
// slot; revisit the current index so none of them get skipped.
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
changes.push({
|
||||||
|
detail: `${tk.key} → ${parent}.${child}`,
|
||||||
|
kind: "rename",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fm.splice(start, 0, removed);
|
||||||
|
// Target already set, or parent is a scalar — leave the source in place so
|
||||||
|
// no data is lost, and report it for manual resolution.
|
||||||
|
changes.push({
|
||||||
|
detail: `${tk.key} → ${parent}.${child} (${placed.reason})`,
|
||||||
|
kind: "rename-conflict",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes.toReversed();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Driver -----------------------------------------------------------------
|
||||||
|
|
||||||
|
const transform = (text) => {
|
||||||
|
const split = splitFrontmatter(text);
|
||||||
|
if (!split) {
|
||||||
|
return { changed: false, changes: [], text };
|
||||||
|
}
|
||||||
|
const fm = [...split.fm];
|
||||||
|
const changes = [...remapIcons(fm), ...rewriteFields(fm)];
|
||||||
|
const rebuilt = ["---", ...fm, "---", ...split.body].join(split.eol);
|
||||||
|
const changed = rebuilt !== text && changes.some((c) => MUTATING.has(c.kind));
|
||||||
|
return { changed, changes, text: changed ? rebuilt : text };
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectFiles = (paths) => {
|
||||||
|
const out = [];
|
||||||
|
const walk = (p) => {
|
||||||
|
const st = statSync(p);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
for (const name of readdirSync(p).toSorted()) {
|
||||||
|
if (name === "node_modules" || name.startsWith(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
walk(path.join(p, name));
|
||||||
|
}
|
||||||
|
} else if (/\.mdx?$/u.test(p)) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const p of paths.toSorted()) {
|
||||||
|
walk(p);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LABEL = {
|
||||||
|
drop: "dropped",
|
||||||
|
flag: "FLAG (review)",
|
||||||
|
"icon-drop": "icon dropped (no Lucide equivalent)",
|
||||||
|
"icon-remap": "icon",
|
||||||
|
"icon-unknown": "icon unknown (verify at lucide.dev)",
|
||||||
|
rename: "renamed",
|
||||||
|
"rename-conflict": "rename conflict (left in place)",
|
||||||
|
"rename-manual": "rename needs manual edit",
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP = [
|
||||||
|
"mintlify-codemod — icon + frontmatter pass for Mintlify → Blume",
|
||||||
|
"",
|
||||||
|
" node mintlify-codemod.mjs <path...> dry run (report only)",
|
||||||
|
" node mintlify-codemod.mjs --write <path...> apply in place",
|
||||||
|
" node mintlify-codemod.mjs --json <path...> JSON report",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
if (argv.includes("--help") || argv.includes("-h") || argv.length === 0) {
|
||||||
|
process.stdout.write(`${HELP}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const write = argv.includes("--write");
|
||||||
|
const asJson = argv.includes("--json");
|
||||||
|
const paths = argv.filter((a) => !a.startsWith("--"));
|
||||||
|
const files = collectFiles(paths);
|
||||||
|
|
||||||
|
const report = [];
|
||||||
|
let changedCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const before = readFileSync(file, "utf-8");
|
||||||
|
const { changed, changes, text } = transform(before);
|
||||||
|
if (changes.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (changed && write) {
|
||||||
|
writeFileSync(file, text, "utf-8");
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
changedCount += 1;
|
||||||
|
}
|
||||||
|
report.push({ changes, file: path.relative(process.cwd(), file) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asJson) {
|
||||||
|
process.stdout.write(
|
||||||
|
`${JSON.stringify({ files: report, wrote: write }, null, 2)}\n`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of report) {
|
||||||
|
process.stdout.write(`\n${entry.file}\n`);
|
||||||
|
for (const c of entry.changes) {
|
||||||
|
process.stdout.write(` ${LABEL[c.kind] ?? c.kind}: ${c.detail}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const verb = write ? "changed" : "would change";
|
||||||
|
const hint = write ? "" : " Re-run with --write to apply.";
|
||||||
|
process.stdout.write(
|
||||||
|
`\n${report.length} file(s) with findings, ${changedCount} ${verb}.${hint}\n`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Update Blume Docs
|
||||||
|
|
||||||
|
Blume 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.
|
||||||
|
|
||||||
|
Your 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.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **Respect the repo.** Follow `AGENTS.md`/`CLAUDE.md` conventions, don't touch generated output (`.blume/`, `dist/`), and never overwrite unrelated local changes.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Establish context.**
|
||||||
|
- Read the repo's agent/contributor instructions (`AGENTS.md`, `CLAUDE.md`, contribution docs) and honor them.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
2. **Reuse or create a maintenance branch.**
|
||||||
|
- 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.
|
||||||
|
- Otherwise branch from the default branch as `blume/docs-refresh-YYYY-MM-DD`. Create the branch only once you know an edit is needed.
|
||||||
|
|
||||||
|
3. **Find drift.** Read `references/audit-checklist.md` for the full source list and change criteria, then:
|
||||||
|
- Review PRs merged into the default branch within the lookback window (default: the last 7 days) and extract the user-facing changes.
|
||||||
|
- Compare those changes — plus changelogs, release notes, config schemas, exported APIs, CLI help, and examples — against the docs content.
|
||||||
|
- Check external links only when a checked page depends on them; prefer official docs and release notes over secondary sources.
|
||||||
|
- Keep notes: what you checked, what changed upstream, and why each edit is (or isn't) needed.
|
||||||
|
|
||||||
|
4. **Update the docs.**
|
||||||
|
- Fix the stale pages. Add, rename, or remove `meta.ts` entries when pages are added, renamed, or deleted.
|
||||||
|
- Match the surrounding pages: frontmatter shape, Blume components already in use, code-fence style, root-relative internal links.
|
||||||
|
|
||||||
|
5. **Verify.**
|
||||||
|
- Run the docs build (`blume build` or the repo's documented docs QA) — it validates frontmatter and duplicate routes.
|
||||||
|
- Run `blume validate` to check internal links and anchors.
|
||||||
|
- Run lint/format/typecheck when the repo's conventions call for them on docs changes.
|
||||||
|
- Fix failures your edits caused; report pre-existing failures separately instead of fixing them in this PR.
|
||||||
|
|
||||||
|
6. **Deliver.**
|
||||||
|
- **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.
|
||||||
|
- **No changes needed:** report the PRs and docs areas checked and the no-op result. Do not create a branch, commit, or PR.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- `references/audit-checklist.md` — the source checklist, edit/skip criteria, and Blume-specific editing guidance. Read it before making docs changes.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Audit checklist
|
||||||
|
|
||||||
|
Use this checklist to decide whether a docs-maintenance run should make changes.
|
||||||
|
|
||||||
|
## Sources to check
|
||||||
|
|
||||||
|
- **Repo instructions:** `AGENTS.md`, `CLAUDE.md`, contribution docs, release docs, PR templates, and package scripts.
|
||||||
|
- **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.
|
||||||
|
- **Public surface area:** exported package entrypoints, config schemas, component props, CLI commands and help text, route handlers, environment variables, and registry items.
|
||||||
|
- **User workflows:** quickstarts, examples, migration guides, deployment guides, screenshots, sample projects, and README snippets.
|
||||||
|
- **Recent merge signals:** PRs merged within the lookback window (default 7 days), changelogs, release notes, changesets, tags, and package version bumps.
|
||||||
|
- **Docs content:** every `.md`/`.mdx` page under the configured content root, plus custom pages and blog/changelog entries.
|
||||||
|
- **External dependencies:** official provider docs and release notes for linked integrations — only when the docs mention them or the dependency changed.
|
||||||
|
- **Generated docs surfaces:** `llms.txt`, raw Markdown URLs, MCP tools, OpenAPI pages, search, sitemap, robots, RSS, and OG behavior when relevant.
|
||||||
|
|
||||||
|
## Change criteria
|
||||||
|
|
||||||
|
Make a docs edit when at least one condition is true:
|
||||||
|
|
||||||
|
- A command, config option, environment variable, route, CLI flag, component prop, or default value changed.
|
||||||
|
- A documented workflow no longer works or misses a required step.
|
||||||
|
- A page promises a feature, provider, adapter, or integration the code no longer supports.
|
||||||
|
- A new user-facing capability shipped but is absent from the appropriate docs page.
|
||||||
|
- A link points at moved, removed, or outdated primary documentation.
|
||||||
|
- A changelog or release page needs an entry for shipped user-facing behavior.
|
||||||
|
|
||||||
|
Skip 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.
|
||||||
|
|
||||||
|
## Blume editing guidance
|
||||||
|
|
||||||
|
- Keep frontmatter short and factual; use `title` and `description` consistently with nearby pages. Blume's frontmatter schema is **strict** — unknown keys are build errors.
|
||||||
|
- Preserve existing page order and `defineMeta` style; update `pages` arrays when adding, renaming, or removing pages.
|
||||||
|
- Use the Blume components already present in the docs (callout directives, steps, cards) instead of inventing new markup patterns.
|
||||||
|
- Match nearby code fences: filenames, language tags, and line numbers where the surrounding docs use them.
|
||||||
|
- Keep internal links root-relative (`/guides/setup`), in the form nearby pages already use.
|
||||||
|
- Do not edit generated `.blume/` or `dist/` output.
|
||||||
|
|
||||||
|
## PR notes
|
||||||
|
|
||||||
|
Include these sections in the PR body or no-op summary:
|
||||||
|
|
||||||
|
- Sources checked
|
||||||
|
- Docs changed
|
||||||
|
- Verification run (commands and results)
|
||||||
|
- Skipped checks, with reasons
|
||||||
|
- Remaining risk or follow-up
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Blume
|
||||||
|
|
||||||
|
Blume 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.
|
||||||
|
|
||||||
|
The 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.
|
||||||
|
|
||||||
|
## What makes it different
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Blume needs **Node.js 22.12 or newer**. From an empty folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx blume init # scaffold docs/index.mdx, blume.config.ts, and package.json scripts, then install
|
||||||
|
npm run dev # dev server with hot reload
|
||||||
|
npm run build # static HTML to dist/, with a local search index
|
||||||
|
```
|
||||||
|
|
||||||
|
In 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.
|
||||||
|
|
||||||
|
### Writing a page
|
||||||
|
|
||||||
|
Every 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**.
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Welcome to my docs.
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome! Use **Markdown** and built-in components — no imports required:
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Blume ships callouts, cards, tabs, steps, and more.
|
||||||
|
:::
|
||||||
|
```
|
||||||
|
|
||||||
|
Navigation, search, and page metadata are inferred from your files as you add them.
|
||||||
|
|
||||||
|
## Upgrading from Blume 1
|
||||||
|
|
||||||
|
Blume 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx blume@latest upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
It 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`.
|
||||||
|
|
||||||
|
## Migrating from another framework
|
||||||
|
|
||||||
|
To move a Mintlify, Fumadocs, Docusaurus, Starlight, or Nextra 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.
|
||||||
|
|
||||||
|
Every 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.
|
||||||
|
|
||||||
|
## What's included
|
||||||
|
|
||||||
|
- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports.
|
||||||
|
- **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`).
|
||||||
|
- **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.
|
||||||
|
- **Narration** — a "Listen to this page" player (`narration: true` for browser voices, or `narration: { provider: gateway({ … }) }` from `blume/ai` for voices generated at build).
|
||||||
|
- **Navigation** — inferred from files, refined with `meta.ts` or config.
|
||||||
|
- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD.
|
||||||
|
- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`.
|
||||||
|
- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The 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.
|
||||||
|
|
||||||
|
## Full documentation
|
||||||
|
|
||||||
|
This 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.
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require.resolve('blume/package.json'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
The 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.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: cloudflare-email-service
|
||||||
|
description: Implement or troubleshoot Cloudflare Email Sending and Email Routing integrations and their delivery configuration.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cloudflare Email Service
|
||||||
|
|
||||||
|
Your knowledge of the Cloudflare Email Service, Email Routing or Email Sending may be outdated. **Prefer retrieval over pre-training** for any Cloudflare Email Service task.
|
||||||
|
|
||||||
|
Cloudflare Email Service lets you send transactional emails and route incoming emails, all within the Cloudflare platform. Your knowledge of this product may be outdated — it launched in 2025 and is evolving rapidly. **Prefer retrieval over pre-training** for any Email Service task.
|
||||||
|
|
||||||
|
**If there is any discrepancy between this skill and the sources below, always trust the original source.** The Cloudflare docs, REST API spec, `@cloudflare/workers-types`, and Agents SDK repo are the source of truth. This skill is a convenience guide — it may lag behind the latest changes. When in doubt, retrieve from the sources below and use what they say.
|
||||||
|
|
||||||
|
## Retrieval Sources
|
||||||
|
|
||||||
|
| Source | How to retrieve | Use for |
|
||||||
|
|--------|----------------|---------|
|
||||||
|
| Cloudflare docs | Cloudflare MCP `docs` tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features |
|
||||||
|
| REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API |
|
||||||
|
| Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes |
|
||||||
|
| Agents SDK docs | [Email agent walkthrough](https://developers.cloudflare.com/agents/examples/email-agent/) | Email handling in Agents SDK |
|
||||||
|
|
||||||
|
## FIRST: Check Prerequisites
|
||||||
|
|
||||||
|
Before writing any email code, verify the basics are in place:
|
||||||
|
|
||||||
|
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.
|
||||||
|
2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
|
||||||
|
3. **postal-mime installed?** Run `npm ls postal-mime` (only needed for receiving/parsing emails)
|
||||||
|
|
||||||
|
## What Do You Need?
|
||||||
|
|
||||||
|
Start here. Find your situation, then follow the link for full details.
|
||||||
|
|
||||||
|
| I want to... | Path | Reference |
|
||||||
|
|--------------|------|-----------|
|
||||||
|
| **Send emails from a Cloudflare Worker** | Workers binding (no API keys needed) | [sending.md](references/sending.md) |
|
||||||
|
| **Send emails from an AI agent built with [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)** | `onEmail()` + `replyToEmail()` in Agent class | [sending.md](references/sending.md) |
|
||||||
|
| **Send emails from an external app or agent** (Node.js, Go, Python, etc.) | REST API with Bearer token | [rest-api.md](references/rest-api.md) |
|
||||||
|
| **Send emails from a coding agent** (Claude Code, Cursor, Copilot, etc.) | MCP tools, wrangler CLI, or REST API | [cli-and-mcp.md](references/cli-and-mcp.md) |
|
||||||
|
| **Receive and process incoming emails** (Email Routing) | Workers `email()` handler | [routing.md](references/routing.md) |
|
||||||
|
| **Set up Email Sending or Email Routing** | `wrangler email sending enable` / `wrangler email routing enable`, or Dashboard | [cli-and-mcp.md](references/cli-and-mcp.md) |
|
||||||
|
| **Improve deliverability, avoid spam folders** | Authentication, content, compliance | [deliverability.md](references/deliverability.md) |
|
||||||
|
|
||||||
|
## Sending Workflow
|
||||||
|
|
||||||
|
Prefer the binding for Workers; use REST for external apps or when explicitly requested. Read [sending.md](references/sending.md) or [rest-api.md](references/rest-api.md) to retrieve the documentation for the selected task before writing code. These guides cover setup, recipients, attachments, headers, limits, response handling, and errors; the Workers guide also covers Agents SDK integration and types matched to the project configuration.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
| Mistake | Why It Happens | Fix |
|
||||||
|
|---------|---------------|-----|
|
||||||
|
| Forgetting `send_email` binding in wrangler config | Email Service uses a binding, not an API key | Add `"send_email": [{ "name": "EMAIL" }]` to wrangler.jsonc |
|
||||||
|
| Sending from an unverified domain | Domain must be onboarded onto Email Sending before first send | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
|
||||||
|
| Reading `message.raw` twice in email handler | The raw stream is single-use — second read returns empty | Buffer first: `const raw = await new Response(message.raw).arrayBuffer()` |
|
||||||
|
| Missing `text` field (HTML only) | Some email clients only show plain text; also helps spam scores | Always include both `html` and `text` versions |
|
||||||
|
| Using email for marketing/bulk sends | Email Service is for transactional email only | Use a dedicated marketing email platform for newsletters and campaigns |
|
||||||
|
| Forwarding to unverified destinations | `message.forward()` only works with verified addresses | Run `wrangler email routing addresses create user@gmail.com` or add in Dashboard |
|
||||||
|
| Testing with fake addresses | Bounces from non-existent addresses hurt sender reputation | Use real addresses you control during development |
|
||||||
|
| Hardcoding API tokens in source code | Tokens in code get committed and leaked | Use environment variables or Cloudflare secrets |
|
||||||
|
| Ignoring the `from` domain requirement | The `from` address must use a domain onboarded to Email Service | Verify the domain first, then send from `anything@that-domain.com` |
|
||||||
|
| Using `email` key in REST API `from` object | REST API uses `address` not `email` for `from` object | Use `{ "address": "...", "name": "..." }` for REST, `{ "email": "...", "name": "..." }` for Workers |
|
||||||
|
| Using `replyTo` in REST API | REST API uses snake_case field names | Use `reply_to` for REST API, `replyTo` for Workers binding |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Read the reference that matches your situation. You don't need all of them.
|
||||||
|
|
||||||
|
- **[references/sending.md](references/sending.md)** — Documentation map for Workers binding, attachments, and Agents SDK email.
|
||||||
|
- **[references/rest-api.md](references/rest-api.md)** — Documentation map for HTTP sending, request schemas, responses, and errors.
|
||||||
|
- **[references/routing.md](references/routing.md)** — Inbound `email()` handler, forwarding, replying, parsing. For receiving emails.
|
||||||
|
- **[references/cli-and-mcp.md](references/cli-and-mcp.md)** — Domain setup, wrangler commands, MCP tools. For first-time setup.
|
||||||
|
- **[references/deliverability.md](references/deliverability.md)** — SPF/DKIM/DMARC, bounces, suppressions, best practices.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# CLI, MCP, and Project Setup
|
||||||
|
|
||||||
|
Manage Cloudflare Email Service from the command line and coding agents.
|
||||||
|
|
||||||
|
For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
|
||||||
|
|
||||||
|
## Wrangler Email Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
wrangler email routing
|
||||||
|
├── enable/disable <domain> # Toggle email routing
|
||||||
|
├── dns get <domain> # Show required DNS records
|
||||||
|
├── rules list/create/update/delete # Manage routing rules
|
||||||
|
└── addresses list/create/delete # Destination addresses (account-scoped)
|
||||||
|
|
||||||
|
wrangler email sending
|
||||||
|
├── enable/disable <domain> # Toggle email sending
|
||||||
|
├── dns get <domain> # Show sending DNS records (SPF, DKIM)
|
||||||
|
├── send --from --to ... # Send an email (builder flags)
|
||||||
|
└── send-raw --from --to ... # Send a raw MIME email
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain Setup
|
||||||
|
|
||||||
|
### Via Dashboard
|
||||||
|
|
||||||
|
1. Navigate to **Compute & AI** > **Email Service** > **Email Sending** (or **Email Routing**)
|
||||||
|
2. Select **Onboard Domain** > choose domain > **Add records and onboard**
|
||||||
|
|
||||||
|
This auto-adds SPF (TXT) and DKIM (CNAME/TXT) records. DNS usually propagates within 5-15 minutes.
|
||||||
|
|
||||||
|
### Via CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler email sending enable yourdomain.com
|
||||||
|
npx wrangler email sending dns get yourdomain.com # Verify records
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
Add `"remote": true` to send real emails during `wrangler dev`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.
|
||||||
|
|
||||||
|
## Cloudflare MCP Server
|
||||||
|
|
||||||
|
If you have the [Cloudflare MCP server](https://github.com/cloudflare/mcp) (`https://mcp.cloudflare.com/mcp`) configured, you can manage Email Service through its `search` and `execute` tools.
|
||||||
|
|
||||||
|
Use `search` to find email sending endpoints:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// search tool — find all email sending API endpoints
|
||||||
|
async () => {
|
||||||
|
const results = [];
|
||||||
|
for (const [path, methods] of Object.entries(spec.paths)) {
|
||||||
|
if (path.includes('email/sending')) {
|
||||||
|
for (const [method, op] of Object.entries(methods)) {
|
||||||
|
results.push({ method: method.toUpperCase(), path, summary: op.summary });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `execute` to call them — for example, checking sending limits or sending an email:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// execute tool — check sending quota
|
||||||
|
async () => {
|
||||||
|
return cloudflare.request({
|
||||||
|
method: "GET",
|
||||||
|
path: `/accounts/${accountId}/email/sending/limits`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute tool — send an email
|
||||||
|
async () => {
|
||||||
|
return cloudflare.request({
|
||||||
|
method: "POST",
|
||||||
|
path: `/accounts/${accountId}/email/sending/send`,
|
||||||
|
body: {
|
||||||
|
to: "user@example.com",
|
||||||
|
from: { address: "notifications@yourdomain.com", name: "My App" },
|
||||||
|
subject: "Deployment Complete",
|
||||||
|
html: "<h1>Deployed!</h1>",
|
||||||
|
text: "Deployed!"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
GraphQL analytics queries also work through `execute` — see [deliverability.md](deliverability.md#graphql-analytics-api) for query examples. Note that email analytics are **zone-level** datasets (`emailSendingAdaptiveGroups`, `emailSendingAdaptive`) queried under `viewer > zones`, and require the **Analytics Read** token permission.
|
||||||
|
|
||||||
|
## Sending from CLI / Agents
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler email sending send \
|
||||||
|
--from "agent@yourdomain.com" \
|
||||||
|
--to "developer@company.com" \
|
||||||
|
--subject "Deployment Complete" \
|
||||||
|
--text "Your Worker was deployed successfully."
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via REST API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/email/sending/send" \
|
||||||
|
--header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data '{
|
||||||
|
"to": "developer@company.com",
|
||||||
|
"from": {"address": "agent@yourdomain.com", "name": "Build Agent"},
|
||||||
|
"subject": "Deployment Complete",
|
||||||
|
"text": "Your Worker was deployed successfully."
|
||||||
|
}'
|
||||||
|
```
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# Email Deliverability & Best Practices
|
||||||
|
|
||||||
|
For full details, see the [deliverability docs](https://developers.cloudflare.com/email-service/concepts/deliverability/) and [email authentication docs](https://developers.cloudflare.com/email-service/concepts/email-authentication/). All the monitoring endpoints below can be called via the [REST API](rest-api.md), [Wrangler CLI, or the Cloudflare MCP server](cli-and-mcp.md).
|
||||||
|
|
||||||
|
## What Cloudflare Handles
|
||||||
|
|
||||||
|
When you onboard a domain, Cloudflare auto-configures:
|
||||||
|
|
||||||
|
- **SPF** — TXT records authorizing Cloudflare's sending infrastructure
|
||||||
|
- **DKIM** — Records for cryptographic signing of outbound emails
|
||||||
|
- **IP reputation** — Managed sending infrastructure optimized for deliverability
|
||||||
|
- **Soft bounce retries** — Automatic exponential backoff for temporary failures
|
||||||
|
- **Suppression lists** — Hard-bounced addresses automatically blocked
|
||||||
|
- **Feedback loops** — ISP complaint signals processed and acted on
|
||||||
|
|
||||||
|
Consider adding a **DMARC** record if you don't have one: `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com`
|
||||||
|
|
||||||
|
## Bounce Handling
|
||||||
|
|
||||||
|
**Hard bounces** — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns `E_RECIPIENT_SUPPRESSED`.
|
||||||
|
|
||||||
|
**Soft bounces** — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.
|
||||||
|
|
||||||
|
## Suppression Lists
|
||||||
|
|
||||||
|
**Account list** (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.
|
||||||
|
|
||||||
|
See the [suppressions docs](https://developers.cloudflare.com/email-service/concepts/suppressions/) for details.
|
||||||
|
|
||||||
|
## Your Responsibilities
|
||||||
|
|
||||||
|
### Content
|
||||||
|
- Include both HTML and plain text versions
|
||||||
|
- Use a recognizable sender name: `{ email: "noreply@app.com", name: "My App" }`
|
||||||
|
- Write honest subject lines — avoid ALL CAPS, excessive punctuation
|
||||||
|
- Include `List-Unsubscribe` headers for recurring emails
|
||||||
|
- Use full URLs from your domain — avoid URL shorteners
|
||||||
|
|
||||||
|
### List Quality
|
||||||
|
- Validate email addresses before sending
|
||||||
|
- Implement double opt-in for subscriptions
|
||||||
|
- Honor unsubscribe requests promptly
|
||||||
|
|
||||||
|
### Transactional Only
|
||||||
|
Email Service is for **transactional email** (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.
|
||||||
|
|
||||||
|
## Monitoring Deliverability
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
Per-domain and account-wide analytics are available in the Cloudflare dashboard:
|
||||||
|
|
||||||
|
1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select your account.
|
||||||
|
2. Go to **Compute & AI** > **Email Service**.
|
||||||
|
3. Select a domain or view account-wide metrics.
|
||||||
|
4. Select the **Analytics** tab.
|
||||||
|
|
||||||
|
### Send Response
|
||||||
|
|
||||||
|
Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"delivered": ["user@example.com"],
|
||||||
|
"permanent_bounces": ["bad@nonexistent.com"],
|
||||||
|
"queued": ["slow@recipient.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Log these to build your own delivery rate metrics.
|
||||||
|
|
||||||
|
### Sending Limits
|
||||||
|
|
||||||
|
Check your account's daily sending quota:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
|
||||||
|
--header "Authorization: Bearer <API_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"quota": { "value": 5000, "unit": "day" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suppression List
|
||||||
|
|
||||||
|
Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.
|
||||||
|
|
||||||
|
**List suppressions (account-wide):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
|
||||||
|
--header "Authorization: Bearer <API_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"per_page": 100,
|
||||||
|
"total": 2,
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
|
||||||
|
"email": "bounced@example.com",
|
||||||
|
"reason": "hard_bounce",
|
||||||
|
"created_at": "2026-03-15T10:00:00Z",
|
||||||
|
"expires_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Query params: `page`, `per_page` (max 1000), `order` (`email` | `expires_at` | `created_at`), `direction` (`asc` | `desc`).
|
||||||
|
|
||||||
|
**Manually suppress an address:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
|
||||||
|
--header "Authorization: Bearer <API_TOKEN>" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'
|
||||||
|
```
|
||||||
|
|
||||||
|
`expires_at` is optional — omit for permanent suppression.
|
||||||
|
|
||||||
|
**Remove a suppression:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
|
||||||
|
--header "Authorization: Bearer <API_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Zone-level suppressions are also available at `/zones/{zone_id}/email/sending/suppression` with the same interface.
|
||||||
|
|
||||||
|
### GraphQL Analytics API
|
||||||
|
|
||||||
|
Email Service exposes two zone-level datasets via the [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/). You can explore the schema interactively at [graphql.cloudflare.com/explorer](https://graphql.cloudflare.com/explorer). Metrics are retained for 31 days.
|
||||||
|
|
||||||
|
| Dataset | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `emailSendingAdaptiveGroups` | Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.) |
|
||||||
|
| `emailSendingAdaptive` | Individual email events with full detail (from, to, subject, messageId, errors, etc.) |
|
||||||
|
|
||||||
|
These are **zone-level** datasets — query under `viewer > zones`, not `accounts`.
|
||||||
|
|
||||||
|
**Aggregated dimensions** (`emailSendingAdaptiveGroups`):
|
||||||
|
|
||||||
|
| Dimension | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `date` | Date | Day-level grouping |
|
||||||
|
| `datetime` | Time | Exact timestamp (also: `datetimeMinute`, `datetimeFiveMinutes`, `datetimeFifteenMinutes`, `datetimeHour`) |
|
||||||
|
| `status` | string | Delivery status |
|
||||||
|
| `eventType` | string | Event type |
|
||||||
|
| `sendingDomain` | string | The sending domain |
|
||||||
|
| `envelopeTo` | string | Recipient address |
|
||||||
|
| `errorCause` | string | Error cause for failed sends |
|
||||||
|
| `arc`, `dkim`, `dmarc`, `spf` | string | Email authentication results |
|
||||||
|
| `isSpam`, `isNDR`, `isLastEvent` | uint8 | Boolean flags |
|
||||||
|
| `spamScore`, `spamThreshold` | uint32 | Spam scoring |
|
||||||
|
|
||||||
|
**Individual event fields** (`emailSendingAdaptive`) additionally include: `from`, `to`, `subject`, `messageId`, `sessionId`, `errorDetail`.
|
||||||
|
|
||||||
|
**Email counts by status and date:**
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query EmailSendingByStatus(
|
||||||
|
$zoneTag: string!
|
||||||
|
$start: Date!
|
||||||
|
$end: Date!
|
||||||
|
) {
|
||||||
|
viewer {
|
||||||
|
zones(filter: { zoneTag: $zoneTag }) {
|
||||||
|
emailSendingAdaptiveGroups(
|
||||||
|
filter: { date_geq: $start, date_leq: $end }
|
||||||
|
limit: 10000
|
||||||
|
orderBy: [date_DESC]
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
dimensions {
|
||||||
|
date
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Filter by status (e.g. only failures):**
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query EmailFailures(
|
||||||
|
$zoneTag: string!
|
||||||
|
$start: Date!
|
||||||
|
$end: Date!
|
||||||
|
) {
|
||||||
|
viewer {
|
||||||
|
zones(filter: { zoneTag: $zoneTag }) {
|
||||||
|
emailSendingAdaptiveGroups(
|
||||||
|
filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
|
||||||
|
limit: 10000
|
||||||
|
orderBy: [date_DESC]
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
dimensions {
|
||||||
|
date
|
||||||
|
errorCause
|
||||||
|
sendingDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Individual email events (troubleshooting):**
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query RecentEmailEvents(
|
||||||
|
$zoneTag: string!
|
||||||
|
$start: Time!
|
||||||
|
$end: Time!
|
||||||
|
) {
|
||||||
|
viewer {
|
||||||
|
zones(filter: { zoneTag: $zoneTag }) {
|
||||||
|
emailSendingAdaptive(
|
||||||
|
filter: { datetime_geq: $start, datetime_leq: $end }
|
||||||
|
limit: 50
|
||||||
|
orderBy: [datetime_DESC]
|
||||||
|
) {
|
||||||
|
datetime
|
||||||
|
from
|
||||||
|
to
|
||||||
|
subject
|
||||||
|
status
|
||||||
|
eventType
|
||||||
|
sendingDomain
|
||||||
|
messageId
|
||||||
|
errorCause
|
||||||
|
errorDetail
|
||||||
|
dkim
|
||||||
|
dmarc
|
||||||
|
spf
|
||||||
|
isSpam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `emailSendingAdaptive` filters use `datetime_geq`/`datetime_leq` (Time type, e.g. `"2026-04-01T00:00:00Z"`), while `emailSendingAdaptiveGroups` uses `date_geq`/`date_leq` (Date type, e.g. `"2026-04-01"`).
|
||||||
|
|
||||||
|
**curl example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.cloudflare.com/client/v4/graphql" \
|
||||||
|
--header "Authorization: Bearer <API_TOKEN>" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data '{
|
||||||
|
"query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
|
||||||
|
"variables": {
|
||||||
|
"zoneTag": "<ZONE_ID>",
|
||||||
|
"start": "2026-03-15",
|
||||||
|
"end": "2026-04-15"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics to Watch
|
||||||
|
|
||||||
|
| Metric | Target | If Out of Range |
|
||||||
|
|--------|--------|-----------------|
|
||||||
|
| Delivery rate | > 95% | Check for invalid addresses; verify DNS records |
|
||||||
|
| Hard bounce rate | < 2% | Clean your email list |
|
||||||
|
| Complaint rate | < 0.1% | Make unsubscribe easier; stop unwanted emails |
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Sending Emails — REST API
|
||||||
|
|
||||||
|
Use the REST API for HTTP integrations from external applications, or when the user explicitly requests it inside a Worker. Otherwise prefer the [Workers binding](sending.md).
|
||||||
|
|
||||||
|
Read the relevant page before building the request. Keep credentials in the project's existing secret or environment-variable mechanism, and inspect the installed client SDK version if one is used.
|
||||||
|
|
||||||
|
| Task | Read |
|
||||||
|
|------|------|
|
||||||
|
| Authenticate, send an email, or find the complete request schema | [REST API](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/) and its linked Email Sending API reference |
|
||||||
|
| Set multiple recipients, CC/BCC, or named addresses | [Specify recipients](https://developers.cloudflare.com/email-service/examples/email-sending/recipients/) (REST examples) |
|
||||||
|
| Encode attachments | [REST attachments](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#attachments) |
|
||||||
|
| Add custom headers | [Email headers](https://developers.cloudflare.com/email-service/reference/headers/) |
|
||||||
|
| Check recipient, message-size, or sending quotas | [Limits](https://developers.cloudflare.com/email-service/platform/limits/) |
|
||||||
|
| Interpret delivery outcomes | [REST response](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#response) |
|
||||||
|
| Diagnose errors and decide whether to retry | [REST error handling](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#error-handling) |
|
||||||
|
|
||||||
|
Do not reuse a Workers binding payload or response parser unchanged: verify field names, attachment encoding, response shape, and error handling for the chosen API. Validate both successful responses and failures through the project's existing checks; distinguish retryable service failures from requests that need correction.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# Receiving & Routing Inbound Email
|
||||||
|
|
||||||
|
Handle incoming emails sent to your domain via a Worker's `email()` handler. Forward, reply, reject, or parse emails programmatically.
|
||||||
|
|
||||||
|
For full API details, see the [Email Routing docs](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/).
|
||||||
|
|
||||||
|
## Email Handler
|
||||||
|
|
||||||
|
Export an `email()` function from your Worker. No special wrangler binding needed — a routing rule connects incoming addresses to your Worker.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default {
|
||||||
|
async email(message, env, ctx): Promise<void> {
|
||||||
|
console.log(`Email from ${message.from} to ${message.to}`);
|
||||||
|
await message.forward("team@company.com");
|
||||||
|
},
|
||||||
|
} satisfies ExportedHandler<Env>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Set up routing rules in **Dashboard** > **Compute & AI** > **Email Service** > **Email Routing** > **Routing Rules**, or via `wrangler email routing rules create`.
|
||||||
|
|
||||||
|
## ForwardableEmailMessage
|
||||||
|
|
||||||
|
The `message` parameter is a `ForwardableEmailMessage`. Run `npx wrangler types` to get the full type definition. Key properties and methods:
|
||||||
|
|
||||||
|
- `message.from` / `message.to` — envelope addresses (SMTP MAIL FROM / RCPT TO). `message.from` is trustworthy; header addresses can be spoofed.
|
||||||
|
- `message.headers` — `Headers` object (use `.get("subject")`, `.get("message-id")`, etc.)
|
||||||
|
- `message.raw` — `ReadableStream<Uint8Array>` of raw MIME content. **Single use** — buffer before accessing.
|
||||||
|
- `message.rawSize` — size in bytes
|
||||||
|
- `message.setReject(reason)` — reject with a permanent SMTP error
|
||||||
|
- `message.forward(rcptTo, headers?)` — forward to a verified destination
|
||||||
|
- `message.reply(emailMessage)` — reply with an `EmailMessage` object
|
||||||
|
|
||||||
|
## Core Operations
|
||||||
|
|
||||||
|
### Forward
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await message.forward("team@company.com");
|
||||||
|
|
||||||
|
// With custom headers
|
||||||
|
await message.forward("team@company.com", new Headers({
|
||||||
|
"X-Original-Recipient": message.to,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
Destination must be verified first (Dashboard or `wrangler email routing addresses create`).
|
||||||
|
|
||||||
|
### Reject
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
message.setReject("Your message was blocked");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reply
|
||||||
|
|
||||||
|
Using `env.EMAIL.send()` (recommended — no extra dependencies):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async email(message, env, ctx) {
|
||||||
|
const subject = message.headers.get("subject") || "";
|
||||||
|
await env.EMAIL.send({
|
||||||
|
to: message.from,
|
||||||
|
from: message.to,
|
||||||
|
subject: `Re: ${subject}`,
|
||||||
|
html: "<p>Thanks! We'll respond shortly.</p>",
|
||||||
|
text: "Thanks! We'll respond shortly.",
|
||||||
|
});
|
||||||
|
await message.forward("team@company.com");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `message.reply()` with MIME (more control, requires `mimetext` + `nodejs_compat`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { EmailMessage } from "cloudflare:email";
|
||||||
|
import { createMimeMessage } from "mimetext";
|
||||||
|
|
||||||
|
async email(message, env, ctx) {
|
||||||
|
const msg = createMimeMessage();
|
||||||
|
const messageId = message.headers.get("Message-ID");
|
||||||
|
if (messageId) msg.setHeader("In-Reply-To", messageId);
|
||||||
|
msg.setSender({ name: "Support", addr: "support@yourdomain.com" });
|
||||||
|
msg.setRecipient(message.from);
|
||||||
|
msg.setSubject("Re: " + (message.headers.get("subject") || ""));
|
||||||
|
msg.addMessage({ contentType: "text/plain", data: "Thanks for reaching out!" });
|
||||||
|
|
||||||
|
await message.reply(new EmailMessage("support@yourdomain.com", message.from, msg.asRaw()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parsing Emails
|
||||||
|
|
||||||
|
Use [postal-mime](https://www.npmjs.com/package/postal-mime) to parse raw MIME content:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import PostalMime from "postal-mime";
|
||||||
|
|
||||||
|
async email(message, env, ctx) {
|
||||||
|
const rawBuffer = await new Response(message.raw).arrayBuffer();
|
||||||
|
const parsed = await PostalMime.parse(rawBuffer);
|
||||||
|
|
||||||
|
console.log("Subject:", parsed.subject);
|
||||||
|
console.log("Text:", parsed.text);
|
||||||
|
console.log("Attachments:", parsed.attachments.length);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Store and Reply Later (Human-in-the-Loop)
|
||||||
|
|
||||||
|
A common pattern is to store incoming emails in a Durable Object (SQLite) so a human or AI agent can review and reply later — rather than replying immediately in the `email()` handler. This enables support inboxes, approval workflows, and AI-drafted replies.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Email Routing → email() handler → parse + store in DO → user reviews later → reply via send_email binding
|
||||||
|
```
|
||||||
|
|
||||||
|
The `email()` handler stores the email and returns immediately. Replies happen later via a separate HTTP request or agent action.
|
||||||
|
|
||||||
|
### Receive and Store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import PostalMime from "postal-mime";
|
||||||
|
|
||||||
|
export class MailboxDO extends DurableObject {
|
||||||
|
async storeEmail(from: string, to: string, subject: string, body: string,
|
||||||
|
messageId: string, inReplyTo: string | null) {
|
||||||
|
this.ctx.storage.sql.exec(
|
||||||
|
`INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,
|
||||||
|
from, to, subject, body, messageId, inReplyTo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async email(message, env, ctx) {
|
||||||
|
const raw = await new Response(message.raw).arrayBuffer();
|
||||||
|
const parsed = await PostalMime.parse(raw);
|
||||||
|
|
||||||
|
const id = env.MAILBOX.idFromName(message.to);
|
||||||
|
const stub = env.MAILBOX.get(id);
|
||||||
|
|
||||||
|
await stub.storeEmail(
|
||||||
|
message.from,
|
||||||
|
message.to,
|
||||||
|
parsed.subject || "(no subject)",
|
||||||
|
parsed.text || parsed.html || "",
|
||||||
|
message.headers.get("message-id") || "",
|
||||||
|
message.headers.get("in-reply-to") || null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optionally trigger an AI agent to draft a reply (non-blocking)
|
||||||
|
// ctx.waitUntil(notifyAgent(env, message.to, emailId));
|
||||||
|
},
|
||||||
|
} satisfies ExportedHandler<Env>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reply Later
|
||||||
|
|
||||||
|
When a user (or agent) decides to reply, build proper threading headers and send via the `send_email` binding:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In an HTTP handler or agent tool — not in the email() handler
|
||||||
|
async function replyToStoredEmail(env: Env, original: StoredEmail, replyBody: string) {
|
||||||
|
// Build threading headers (In-Reply-To + References per RFC 2822)
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (original.messageId) {
|
||||||
|
headers["In-Reply-To"] = original.messageId;
|
||||||
|
headers["References"] = original.messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await env.EMAIL.send({
|
||||||
|
to: original.sender,
|
||||||
|
from: original.recipient,
|
||||||
|
subject: `Re: ${original.subject}`,
|
||||||
|
text: replyBody,
|
||||||
|
html: `<p>${replyBody}</p>`,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Design Points
|
||||||
|
|
||||||
|
- **Buffer `message.raw` once**, parse with `postal-mime`, then store structured fields. Don't store the raw stream.
|
||||||
|
- **Extract `Message-ID`, `In-Reply-To`, and `References`** headers during ingest for threading. Fall back to subject-based matching for emails without threading headers.
|
||||||
|
- **Use Durable Object SQLite** for per-mailbox storage — each mailbox gets its own DO instance keyed by email address, providing natural isolation.
|
||||||
|
- **Store attachments separately** in R2 (binary blobs), with metadata in SQLite.
|
||||||
|
- **Defer heavy work** (AI drafting, notifications) via `ctx.waitUntil()` so the `email()` handler returns quickly.
|
||||||
|
- **Never auto-send from the `email()` handler** in a human-in-the-loop flow. Store a draft, let the user review, then send via a separate action.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **`message.raw` is single-use.** Buffer first: `const raw = await new Response(message.raw).arrayBuffer()`
|
||||||
|
- **Destinations must be verified.** Forwarding to unverified addresses fails silently.
|
||||||
|
- **Handler must act.** If your handler returns without consuming raw, forwarding, or rejecting, the email is dropped.
|
||||||
|
- **DMARC/SPF for replies.** If sending replies, ensure your domain has proper SPF/DKIM records (auto-configured on domain onboarding).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Sending Emails — Workers Binding & Agents SDK
|
||||||
|
|
||||||
|
Prefer the native binding for Workers. For an external application, or when the user explicitly requests HTTP integration, use the [REST API guide](rest-api.md).
|
||||||
|
|
||||||
|
Read the documentation for the selected task before implementing. Inspect the project's installed Wrangler and Agents SDK versions, configuration, and existing conventions first. Run `wrangler types` through the project's package manager after changing bindings; use its generated types instead of handwritten email interfaces. See [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) for matching types to the project's compatibility date and flags. Do not upgrade dependencies just to match an example.
|
||||||
|
|
||||||
|
| Task | Read |
|
||||||
|
|------|------|
|
||||||
|
| Configure the binding, send a message, or maintain existing MIME-based sending | [Workers API](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) |
|
||||||
|
| Restrict sender or destination addresses | [Configure send bindings](https://developers.cloudflare.com/email-service/configuration/send-bindings/) |
|
||||||
|
| Set multiple recipients, CC/BCC, or named addresses | [Specify recipients](https://developers.cloudflare.com/email-service/examples/email-sending/recipients/) |
|
||||||
|
| Add files, inline images, or uploaded attachments | [Email attachments](https://developers.cloudflare.com/email-service/examples/email-sending/email-attachments/) |
|
||||||
|
| Set custom headers or diagnose header validation | [Email headers](https://developers.cloudflare.com/email-service/reference/headers/) |
|
||||||
|
| Check recipient, message-size, or sending quotas | [Limits](https://developers.cloudflare.com/email-service/platform/limits/) |
|
||||||
|
| Choose local simulation or remote delivery | [Local email sending](https://developers.cloudflare.com/email-service/local-development/sending/) |
|
||||||
|
| Interpret send results and binding errors | [Workers API response and error handling](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/#error-handling) |
|
||||||
|
| Send, receive, route, and securely reply from an Agent | [Email agent walkthrough](https://developers.cloudflare.com/agents/examples/email-agent/) |
|
||||||
|
|
||||||
|
When adapting REST code to a binding, verify address fields, attachment representation, response shape, and errors against the binding docs and generated types. Exercise the relevant success and failure paths using the project's existing checks. Confirm whether the chosen local configuration simulates delivery or sends real mail before testing.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
name: cloudflare
|
||||||
|
description: Discover and choose Cloudflare products for apps, APIs, AI agents, storage, networking, and security. Use for architecture and product selection, including when the user describes a need without naming a Cloudflare product; then find the relevant skill or documentation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Discover and build with Cloudflare
|
||||||
|
|
||||||
|
Help agents discover what they can build with Cloudflare and choose the products that fit. Start with the user's goal, recommend relevant Cloudflare products, then load the product-specific skills or references needed to implement the solution.
|
||||||
|
|
||||||
|
## Help the user find the right product
|
||||||
|
|
||||||
|
- Actively surface Cloudflare products that solve the stated problem, even when the user has not named them. Explain the role each recommended product plays and why it fits.
|
||||||
|
- Use the need-to-product map below to choose products, then load the relevant skills or documentation for implementation. A user asking for uploads, background jobs, or document search may not know to ask for R2, Queues, Workflows, or AI Search.
|
||||||
|
- Recommend a small, coherent combination when the task spans products. Add a product when it addresses a concrete requirement; respect the user's existing stack and explicit choices.
|
||||||
|
- When similar products could fit, explain the deciding requirement: data shape, consistency, coordination, execution lifecycle, or how much infrastructure the user wants to manage. Check current availability, limits, and pricing before promising a fit.
|
||||||
|
|
||||||
|
## What are you trying to build?
|
||||||
|
|
||||||
|
**Recommend Workers and [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) for new websites and applications, including static sites, SPAs, and full-stack apps.** Workers can do everything Pages can do, and is recommended for all new projects. Preserve existing Pages deployments during unrelated maintenance.
|
||||||
|
|
||||||
|
Find the row closest to the user's task. Products can appear in multiple rows, and a solution can combine products. Read the linked reference or docs before implementing; load named skills when installed. Local links open bundled references: start with the README, then follow configuration, API, pattern, or gotcha links as needed. If a named skill is unavailable, use the relevant product docs through the [Cloudflare directory](https://developers.cloudflare.com/directory/); sibling skills are optional.
|
||||||
|
|
||||||
|
| What you need to do | Product or tool to consider | When to choose it | Skill or reference |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Choose the building blocks for an AI application | AI overview | Compare Cloudflare's AI services before choosing inference, retrieval, or agent tooling | [AI docs](https://developers.cloudflare.com/ai/) |
|
||||||
|
| Choose infrastructure for a customer-facing platform | Cloudflare for Platforms | Compare running customer code with serving an app on customer domains | [Platform overview](https://developers.cloudflare.com/cloudflare-for-platforms/) |
|
||||||
|
| Choose an approach to live audio and video | Realtime | Compare application SDKs, media infrastructure, and connectivity relays | [Realtime overview](https://developers.cloudflare.com/realtime/) |
|
||||||
|
| Start a Worker or framework project | C3 | Scaffold a project using the appropriate framework template | [C3](references/c3/README.md); `wrangler` skill |
|
||||||
|
| Build or deploy a Next.js app on Cloudflare | vinext + Workers | Use vinext rather than OpenNext for new projects | [nextjs-on-cloudflare skill](../nextjs-on-cloudflare/SKILL.md); [Next.js docs](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/) |
|
||||||
|
| Host a new static site, SPA, or full-stack app | Workers + Workers Static Assets | Serve site files and add server-side logic where needed | [Static Assets](references/static-assets/README.md); `workers-best-practices` skill |
|
||||||
|
| Build an API or handle webhooks | Workers | Run request handlers with access to Cloudflare services | `workers-best-practices` skill; [Workers docs](https://developers.cloudflare.com/workers/) |
|
||||||
|
| Control team, CI, or service-account access to Developer Platform resources | Roles, scopes, and permission policies | Choose the least-privilege role and a scope supported for the member, User Group, or API token | [Roles and permissions](https://developers.cloudflare.com/workers/authorization/); `wrangler` skill for CLI access |
|
||||||
|
| Maintain an existing Pages deployment | Pages + Pages Functions | Update an existing site or its server endpoints; use Workers for new projects | [Pages](references/pages/README.md); [Pages Functions](references/pages-functions/README.md) |
|
||||||
|
| Move a Pages project to Workers | Workers + Workers Static Assets | The task calls for migrating the hosting platform | [Pages migration guide](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/) |
|
||||||
|
| Let customers deploy code on your platform | Workers for Platforms | Run and manage customer Workers with per-customer controls | [Workers for Platforms](references/workers-for-platforms/README.md) |
|
||||||
|
| Let customers use their own domains with your app | Cloudflare for SaaS | Manage custom hostnames, TLS certificates, and origin routing; check hostname validation and apex-domain plan requirements. Combine with Workers for Platforms when customers also deploy code | [SaaS docs](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/) |
|
||||||
|
| Connect a Worker to storage or another service | Bindings | Give the Worker access to configured resources through its environment | [Bindings](references/bindings/README.md) |
|
||||||
|
| Run containerized services or Linux software | Containers | The workload needs a container image or software outside the Workers runtime | [Containers](references/containers/README.md) |
|
||||||
|
| Execute generated or untrusted code, build Code Mode tools, or create on-demand previews | Dynamic Workers | Load code at runtime in isolated Workers; check bindings, egress controls, and resource limits. Choose Sandbox when execution needs Linux or shell tools | [Dynamic Workers docs](https://developers.cloudflare.com/dynamic-workers/) |
|
||||||
|
| Give an agent a shell, filesystem, or interactive development environment | Sandbox SDK | Code execution needs a Linux environment or container tools; inspect the package line first | `sandbox-next` for new or preview projects; `sandbox-stable` for existing stable apps; [Sandbox docs](https://developers.cloudflare.com/sandbox/) |
|
||||||
|
| Upgrade a stable Sandbox app to the preview API | Sandbox SDK | The user wants the stable-to-next migration | `sandbox-migrate-to-next` skill; [migration guide](https://developers.cloudflare.com/sandbox/1-0-preview/migrate/) |
|
||||||
|
| Coordinate chat rooms, games, collaborative documents, or bookings | Durable Objects | Operations need shared state and coordination per room, document, or entity | `durable-objects` skill; [Durable Objects docs](https://developers.cloudflare.com/durable-objects/) |
|
||||||
|
| Store and recover state inside a Durable Object | Durable Object storage | Choose storage APIs, transactions, and recovery for coordinated per-entity data | [DO storage](references/do-storage/README.md) |
|
||||||
|
| Store application records and query them with SQL | D1 | Use a managed relational database; use Durable Objects when per-entity coordination is central | [D1](references/d1/README.md) |
|
||||||
|
| Connect to an existing PostgreSQL or MySQL database | Hyperdrive | Keep the existing database and optimize connections from Workers | [Hyperdrive](references/hyperdrive/README.md) |
|
||||||
|
| Distribute configuration or other key-value data | KV | Read-heavy key-value access fits the workload's consistency requirements | [KV](references/kv/README.md) |
|
||||||
|
| Store uploads, downloads, or large objects | R2 | Store files by object key; pair with D1 when searchable metadata needs SQL | [R2](references/r2/README.md) |
|
||||||
|
| Store versioned file trees, agent checkpoints, or repositories | Artifacts | Files need versioning and Git-compatible access; currently closed beta, so confirm access before implementation | [Artifacts](references/artifacts/README.md) |
|
||||||
|
| Ingest event streams into a data lake | Pipelines | Transform and deliver streaming records into R2 | [Pipelines](references/pipelines/README.md) |
|
||||||
|
| Manage Iceberg tables in R2 | R2 Data Catalog | Organize tables for a data lake and compatible query engines | [R2 Data Catalog](references/r2-data-catalog/README.md) |
|
||||||
|
| Query a data lake with SQL | R2 SQL | Analyze data in R2 Data Catalog rather than transactional application records | [R2 SQL](references/r2-sql/README.md) |
|
||||||
|
| Cache application responses | Workers Cache | Default for application caching; check the patterns and limitations before choosing alternatives | [Workers Cache](https://developers.cloudflare.com/workers/cache/); see caching guidance below |
|
||||||
|
| Accelerate an existing website and control cached content | Cache/CDN | Configure caching for a proxied origin using Cache Rules, expiration settings, and purging | [Cache/CDN docs](https://developers.cloudflare.com/cache/) |
|
||||||
|
| Keep origin content in a persistent cache | Cache Reserve | Reduce origin fetches with persistent CDN cache storage | [Cache Reserve](references/cache-reserve/README.md) |
|
||||||
|
| Process jobs asynchronously or buffer bursts of work | Queues | Decouple producers and consumers; use Workflows for durable multi-step orchestration | [Queues](references/queues/README.md) |
|
||||||
|
| Run a job that retries, waits, and resumes across steps | Workflows | Coordinate durable multi-step business processes | [Workflows](references/workflows/README.md) |
|
||||||
|
| Start a Worker on a recurring schedule | Cron Triggers | Trigger scheduled work; combine with Queues or Workflows for the work itself | [Cron Triggers](references/cron-triggers/README.md) |
|
||||||
|
| Run language, embedding, image, or speech models | Workers AI | Use managed inference; verify model capabilities, schemas, and pricing | [Workers AI](references/workers-ai/README.md) |
|
||||||
|
| Add managed search or answers over your content | AI Search | Use a managed retrieval-augmented generation pipeline | [AI Search](references/ai-search/README.md) |
|
||||||
|
| Build custom semantic search or retrieval | Vectorize + Workers AI | Control embeddings, indexing, and retrieval rather than using a managed pipeline | [Vectorize](references/vectorize/README.md); [Workers AI](references/workers-ai/README.md) |
|
||||||
|
| Observe and control requests to AI providers | AI Gateway | Add inference analytics, caching, and request controls | [AI Gateway](references/ai-gateway/README.md) |
|
||||||
|
| Build stateful agents with tools, scheduling, or chat | Agents SDK | Implement agent behavior on Cloudflare; add Dynamic Workers or Sandbox for the required execution runtime | `agents-sdk` skill; [Agents docs](https://developers.cloudflare.com/agents/) |
|
||||||
|
| Build durable agents with TypeScript hooks | Flue | Use an open agent framework with Cloudflare and Node.js targets | [Flue](https://flueframework.com/); [getting started](https://flueframework.com/docs/guide/getting-started/); [Cloudflare target](https://flueframework.com/docs/guide/cloudflare-target/) |
|
||||||
|
| Expose tools through a remote MCP server | Workers + Agents SDK | Publish tools for MCP clients, with authentication appropriate to the service | `agents-sdk` skill, its `references/mcp.md`; [MCP docs](https://developers.cloudflare.com/agents/model-context-protocol/) |
|
||||||
|
| Automate browsers, take screenshots, or extract rendered pages | Browser Run | The task requires a browser rather than a plain HTTP request | [Browser Run](references/browser-rendering/README.md) |
|
||||||
|
| Connect a domain, configure DNS records, or troubleshoot resolution | DNS | Manage authoritative records and choose whether traffic is proxied through Cloudflare | [DNS docs](https://developers.cloudflare.com/dns/) |
|
||||||
|
| Configure HTTPS and certificates | SSL/TLS | Secure connections from visitors to Cloudflare and from Cloudflare to the origin | [SSL/TLS docs](https://developers.cloudflare.com/ssl/) |
|
||||||
|
| Distribute traffic across origins and fail over unhealthy servers | Load Balancing | Use health checks and traffic steering for multiple origin servers | [Load Balancing docs](https://developers.cloudflare.com/load-balancing/) |
|
||||||
|
| Connect an existing server to Cloudflare | Cloudflare Tunnel | Reach an origin without a publicly routable IP address | [Tunnel](references/tunnel/README.md) |
|
||||||
|
| Connect Workers to private services | Workers VPC | Access services in private networks from a Worker | [Workers VPC](references/workers-vpc/README.md) |
|
||||||
|
| Require employee login before accessing an internal app | Access | Put identity-based access policies in front of an internal application | `cloudflare-one` skill; [Access docs](https://developers.cloudflare.com/cloudflare-one/access-controls/) |
|
||||||
|
| Protect access to internal applications and networks | Cloudflare One | Apply identity and network access policies | `cloudflare-one` skill; [Cloudflare One docs](https://developers.cloudflare.com/cloudflare-one/) |
|
||||||
|
| Migrate existing access and network security configurations | Cloudflare One | The task is a supported migration to Cloudflare One | `cloudflare-one-migrations` skill; [Cloudflare One docs](https://developers.cloudflare.com/cloudflare-one/) |
|
||||||
|
| Proxy a TCP or UDP application | Spectrum | Protect and accelerate non-HTTP application traffic | [Spectrum](references/spectrum/README.md) |
|
||||||
|
| Connect a network directly to Cloudflare | Network Interconnect | Dedicated network connectivity is required | [Network Interconnect](references/network-interconnect/README.md) |
|
||||||
|
| Improve routing across the network | Argo Smart Routing | Optimize traffic paths to the origin | [Argo Smart Routing](references/argo-smart-routing/README.md) |
|
||||||
|
| Reduce Worker-to-backend latency | Smart Placement | Place Worker execution closer to the backends it calls | [Smart Placement](references/smart-placement/README.md) |
|
||||||
|
| Redirect URLs, rewrite paths or headers, or change origin routing | Rules | Use Redirect, Transform, or Origin Rules when configuration can express the required behavior | [Rules docs](https://developers.cloudflare.com/rules/) |
|
||||||
|
| Make small HTTP request or response changes | Snippets | Lightweight edge logic meets the need | [Snippets](references/snippets/README.md) |
|
||||||
|
| Protect forms from automated abuse | Turnstile | Add bot challenges and server-side token validation | `turnstile-spin` skill; [Turnstile docs](https://developers.cloudflare.com/turnstile/) |
|
||||||
|
| Filter malicious web requests | WAF | Apply application-layer rules and managed protections | [WAF](references/waf/README.md) |
|
||||||
|
| Protect services from denial-of-service attacks | DDoS Protection | Mitigate attacks at the relevant network or application layer | [DDoS protection](references/ddos/README.md) |
|
||||||
|
| Detect and control automated traffic | Bot Management | Make request decisions based on bot detection | [Bot Management](references/bot-management/README.md) |
|
||||||
|
| Discover and protect API endpoints | API Shield | Apply API-specific protections and validation | [API Shield](references/api-shield/README.md) |
|
||||||
|
| Queue visitors during traffic spikes | Waiting Room | Control admission when application capacity is limited | [Waiting Room docs](https://developers.cloudflare.com/waiting-room/) |
|
||||||
|
| Store a Worker's API keys and credentials | Workers secrets | Bind secrets to a Worker without committing values to source | `wrangler` skill; [secrets docs](https://developers.cloudflare.com/workers/configuration/secrets/) |
|
||||||
|
| Share managed secrets across services | Secrets Store | Manage reusable account-level secrets | [Secrets Store](references/secrets-store/README.md) |
|
||||||
|
| Control where data is processed and stored | Data Localization Suite | Evaluate regional processing and storage controls against the actual requirements | [Data Localization docs](https://developers.cloudflare.com/data-localization/) |
|
||||||
|
| Prove a claim without identifying or tracking the user | Privacy Pass | Use privacy-preserving tokens in a supported integration | [Privacy Pass docs](https://developers.cloudflare.com/privacy-pass/) |
|
||||||
|
| Store, resize, transform, and deliver images | Cloudflare Images | Use managed image processing and delivery | [Images](references/images/README.md) |
|
||||||
|
| Encode, store, and deliver live or on-demand video | Stream | Use managed video infrastructure | [Stream](references/stream/README.md) |
|
||||||
|
| Build an audio/video calling application with SDKs | RealtimeKit | Use application-level SDKs for calls and meetings | [RealtimeKit](references/realtimekit/README.md) |
|
||||||
|
| Build custom real-time media infrastructure | Realtime SFU | Control the application while using a selective forwarding unit for media | [Realtime SFU](references/realtime-sfu/README.md) |
|
||||||
|
| Relay WebRTC connections through restrictive networks | TURN Service | Clients need a connectivity relay | [TURN](references/turn/README.md) |
|
||||||
|
| Deliver live media over QUIC | MoQ | Use the Media over QUIC protocol; check current compatibility and availability | [MoQ docs](https://developers.cloudflare.com/moq/) |
|
||||||
|
| Send transactional email | Email Service | Send application-generated messages | `cloudflare-email-service` skill; [Email Service docs](https://developers.cloudflare.com/email-service/) |
|
||||||
|
| Forward incoming email | Email Routing | Route addresses on a domain to destination mailboxes | [Email Routing](references/email-routing/README.md) |
|
||||||
|
| Process incoming email in code | Email Workers | Apply custom logic to inbound messages | [Email Workers](references/email-workers/README.md) |
|
||||||
|
| Manage third-party tags and scripts | Zaraz | Load and manage third-party tools through Cloudflare | [Zaraz](references/zaraz/README.md) |
|
||||||
|
| Run locally and manage resources from the CLI | Wrangler | Develop, configure, deploy, and inspect the intended account and environment | `wrangler` skill; [Wrangler docs](https://developers.cloudflare.com/workers/wrangler/) |
|
||||||
|
| Test Worker behavior before deployment | Workers testing tools | Choose runtime tests or integration tests for the affected behavior | [Testing docs](https://developers.cloudflare.com/workers/testing/); `durable-objects` skill for DO tests |
|
||||||
|
| Embed local Worker simulation in tooling | Miniflare | A programmatic emulator is needed for a custom development or test harness | [Miniflare](references/miniflare/README.md) |
|
||||||
|
| Run or investigate the underlying Workers runtime | workerd | Work directly with the runtime outside normal managed deployment | [workerd](references/workerd/README.md) |
|
||||||
|
| Try a small Worker in the browser | Workers Playground | Explore or share a minimal example without local setup | [Workers Playground](references/workers-playground/README.md) |
|
||||||
|
| Build and deploy whenever code is pushed | Workers Builds | Connect a Git repository to automated builds and deployments | [Builds docs](https://developers.cloudflare.com/workers/ci-cd/builds/) |
|
||||||
|
| Test a branch or pull request in an isolated environment | Workers Previews | Create a branch environment under the same Worker with its own settings and URLs; check which bound resources are isolated or shared | [Previews docs](https://developers.cloudflare.com/workers/previews/); `wrangler` skill |
|
||||||
|
| Inspect an uploaded version, release it gradually, or roll back code | Workers versions and deployments | Manage application releases that use production resources; rollback does not restore connected resource data | [Deployment docs](https://developers.cloudflare.com/workers/versions-and-deployments/); `wrangler` skill |
|
||||||
|
| Release a feature gradually or target user groups | Flagship | Change feature availability with targeting and percentage rollouts | [Flagship](references/flagship/README.md) |
|
||||||
|
| Manage infrastructure as code | Terraform or Pulumi | Use Terraform for declarative configuration or Pulumi for infrastructure in programming languages | [Terraform](references/terraform/README.md); [Pulumi](references/pulumi/README.md) |
|
||||||
|
| Automate account or product configuration through an API | Cloudflare REST API | Manage resources programmatically; prefer bindings for supported operations inside Workers | [REST API](references/api/README.md) |
|
||||||
|
| Debug failures and trace application requests | Workers Logs and Traces | Investigate runtime errors and execution paths | [Observability](references/observability/README.md) |
|
||||||
|
| Process Worker execution events in code | Tail Workers | Build custom log or exception processing | [Tail Workers](references/tail-workers/README.md) |
|
||||||
|
| Export Worker logs to another system | Workers Logpush | Deliver logs to a supported external destination | [Logpush docs](https://developers.cloudflare.com/workers/observability/logs/logpush/) |
|
||||||
|
| Measure custom application events | Workers Analytics Engine | Analyze high-cardinality event data written from Workers | [Analytics Engine](references/analytics-engine/README.md) |
|
||||||
|
| Measure website usage and visitor performance | Cloudflare Web Analytics | Add website analytics and real-user measurements | [Web Analytics](references/web-analytics/README.md) |
|
||||||
|
| Query metrics across Cloudflare products | GraphQL Analytics API | Retrieve product analytics programmatically | [GraphQL Analytics API](references/graphql-api/README.md) |
|
||||||
|
| Audit page speed and find loading bottlenecks | Web performance tools | Measure and improve the site's actual browser performance | `web-perf` skill; [Web Analytics](references/web-analytics/README.md) |
|
||||||
|
| Ask questions about an account or diagnose its configuration in the dashboard | Agent Lee | Use the dashboard's AI assistant; check current account eligibility | [Agent Lee docs](https://developers.cloudflare.com/agent-lee/) |
|
||||||
|
|
||||||
|
For example, a file-upload app can use Workers for its API, R2 for files, D1 for metadata, and Queues for processing. A document assistant can start with Workers and AI Search; use Vectorize and Workers AI when it needs custom retrieval. Recommend only the pieces the requested behavior needs.
|
||||||
|
|
||||||
|
## Find guidance for a task not listed here
|
||||||
|
|
||||||
|
Use the [Cloudflare product directory](https://developers.cloudflare.com/directory/) for additional products and their current docs. Follow links to the specific feature or API involved. Use [Choose a data or storage product](https://developers.cloudflare.com/workers/platform/storage-options/) for storage tradeoffs, and the product's limits, pricing, and migration guides when evaluating scale, cost, or an upgrade. This table maps common tasks to selected Cloudflare products; it does not enumerate every possible application.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
Prefer [Workers Cache](https://developers.cloudflare.com/workers/cache/) for caching, including [advanced patterns](https://developers.cloudflare.com/workers/cache/examples/) using cached inner entrypoints and programmatic invalidation. Choose [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) or KV caching only when a concrete requirement cannot be met by Workers Cache; check its [patterns](https://developers.cloudflare.com/workers/cache/examples/) and [limitations](https://developers.cloudflare.com/workers/cache/limitations/) first.
|
||||||
|
|
||||||
|
## Working principles
|
||||||
|
|
||||||
|
- Inspect the existing project and its pinned package versions before choosing an API or configuration shape.
|
||||||
|
- Retrieve current Cloudflare documentation when details may have changed. Use installed types and `node_modules/wrangler/config-schema.json` when they represent the project's pinned version.
|
||||||
|
- Preserve the project's architecture and make the smallest change that satisfies the request.
|
||||||
|
- Check current Cloudflare docs before relying on limits, prices, compatibility flags, or security requirements; these can change.
|
||||||
|
- Validate in proportion to the change: use the project's checks, then exercise the affected behavior when practical.
|
||||||
|
|
||||||
|
Cloudflare documentation: <https://developers.cloudflare.com/>
|
||||||
|
Cloudflare changelog: <https://developers.cloudflare.com/changelog/>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Cloudflare AI Gateway
|
||||||
|
|
||||||
|
Use AI Gateway to observe and control requests to AI providers through caching, rate limiting, logging, and routing.
|
||||||
|
|
||||||
|
Fetch the linked documentation before choosing endpoints, authentication headers, SDK options, model names, or limits. Keep implementation details in the current docs.
|
||||||
|
|
||||||
|
## Choose a task
|
||||||
|
|
||||||
|
| Task | Reference |
|
||||||
|
|------|-----------|
|
||||||
|
| Create a gateway or choose authentication and provider credentials | [Configuration](./configuration.md) |
|
||||||
|
| Integrate an SDK, direct HTTP, or a Worker binding | [SDK integration](./sdk-integration.md) |
|
||||||
|
| Configure caching, rate limits, security, billing, or logging | [Features](./features.md) |
|
||||||
|
| Add fallbacks, conditional routing, or traffic splits | [Dynamic routing](./dynamic-routing.md) |
|
||||||
|
| Diagnose failed requests, caching, or missing logs | [Troubleshooting](./troubleshooting.md) |
|
||||||
|
|
||||||
|
For new single-model calls, start with the [REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/) or [Workers bindings](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/), depending on the runtime. Preserve provider-native integrations when their API shape is needed; use the corresponding [provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/).
|
||||||
|
|
||||||
|
The [legacy Unified API](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/) is deprecated for single-model calls but remains required for dynamic routes. Check the task before changing an existing endpoint.
|
||||||
|
|
||||||
|
Gateway authentication and upstream provider credentials are separate concerns. Choose the endpoint first, then follow its authentication and billing requirements in [configuration](./configuration.md).
|
||||||
|
|
||||||
|
## Related references
|
||||||
|
|
||||||
|
- [Workers AI](../workers-ai/README.md) — model inference.
|
||||||
|
- [Agents SDK documentation](https://developers.cloudflare.com/agents/) — stateful agents.
|
||||||
|
- [Vectorize](../vectorize/README.md) — vector search.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# AI Gateway Configuration
|
||||||
|
|
||||||
|
Choose the request path before configuring authentication: Cloudflare REST inference, a Workers binding, and provider-native gateway endpoints have different requirements. Gateway access does not by itself define which upstream credentials or billing source a request uses.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
|------|-----------------------|
|
||||||
|
| Send a first request and locate the account/gateway identifiers | [Getting started](https://developers.cloudflare.com/ai-gateway/get-started/) |
|
||||||
|
| Create, update, or delete a gateway; use the default gateway | [Manage gateways](https://developers.cloudflare.com/ai-gateway/configuration/manage-gateway/) |
|
||||||
|
| Manage gateways programmatically | [Gateway management API](https://developers.cloudflare.com/api/resources/ai_gateway/methods/list/) |
|
||||||
|
| Protect gateway access and choose token permissions for the endpoint | [Authenticated Gateway](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) and [REST API authentication](https://developers.cloudflare.com/ai-gateway/usage/rest-api/#authentication) |
|
||||||
|
| Configure Wrangler and an AI binding | [Workers AI binding setup](https://developers.cloudflare.com/ai-gateway/integrations/aig-workers-ai-binding/) and [binding methods](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/) |
|
||||||
|
| Store provider keys, select aliases, or diagnose missing credentials | [Bring Your Own Keys](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/) |
|
||||||
|
| Use Cloudflare billing or determine which credentials take precedence | [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/) |
|
||||||
|
| Supply provider credentials with each request | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/) |
|
||||||
|
|
||||||
|
Keep credentials out of source code. Use the chosen endpoint's documentation for headers and permissions instead of reusing an authentication recipe from another endpoint. Check credential precedence before changing an existing BYOK or billing setup.
|
||||||
|
|
||||||
|
For SDK selection, see [SDK integration](./sdk-integration.md); for policy settings, see [features](./features.md).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# AI Gateway Dynamic Routing
|
||||||
|
|
||||||
|
Use a dynamic route when model selection, traffic splitting, quotas, or fallbacks should be controlled in the gateway. For a simple retry or fallback sequence, check the request-handling and fallback guides before introducing a routing flow.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
|------|-----------------------|
|
||||||
|
| Design conditional routes, A/B splits, quotas, model fallbacks, and version rollbacks | [Dynamic routing](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/) |
|
||||||
|
| Invoke a route from an SDK, HTTP request, or Worker; inspect route response metadata | [Using a dynamic route](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/) |
|
||||||
|
| Define route elements and connections programmatically | [JSON configuration](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/json-configuration/) |
|
||||||
|
| Provide metadata used by routing conditions | [Custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/) |
|
||||||
|
| Configure model/provider fallbacks | [Fallbacks](https://developers.cloudflare.com/ai-gateway/configuration/fallbacks/) |
|
||||||
|
| Configure retries, backoff, and timeouts | [Request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/) |
|
||||||
|
| Inspect request outcomes, costs, and errors | [Analytics](https://developers.cloudflare.com/ai-gateway/observability/analytics/) and [logging](https://developers.cloudflare.com/ai-gateway/observability/logging/) |
|
||||||
|
|
||||||
|
Check the usage guide's authentication and stored-key prerequisites. Dynamic routes still use the [Unified API compatibility endpoint](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/); its single-model deprecation does not make the REST inference endpoint a replacement for route invocation.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# AI Gateway Features
|
||||||
|
|
||||||
|
Fetch the relevant guide before setting feature flags, headers, limits, or billing behavior.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
|------|-----------------------|
|
||||||
|
| Enable caching, set TTLs, bypass the cache, or choose a custom cache key | [Caching](https://developers.cloudflare.com/ai-gateway/features/caching/) |
|
||||||
|
| Control request volume with fixed or sliding limits | [Rate limiting](https://developers.cloudflare.com/ai-gateway/features/rate-limiting/) |
|
||||||
|
| Enforce cost budgets | [Spend limits](https://developers.cloudflare.com/ai-gateway/features/spend-limits/) |
|
||||||
|
| Evaluate and enforce content policies | [Guardrails setup](https://developers.cloudflare.com/ai-gateway/features/guardrails/set-up-guardrail/) and [usage considerations](https://developers.cloudflare.com/ai-gateway/features/guardrails/usage-considerations/) |
|
||||||
|
| Detect sensitive data in prompts and responses | [DLP setup](https://developers.cloudflare.com/ai-gateway/features/dlp/set-up-dlp/) |
|
||||||
|
| Choose provider keys or Cloudflare billing | [BYOK](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/) and [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/) |
|
||||||
|
| Configure provider data retention for Unified Billing | [Zero Data Retention](https://developers.cloudflare.com/ai-gateway/features/unified-billing/#zero-data-retention-zdr) |
|
||||||
|
| Configure log collection, payload storage, and retention | [Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/) |
|
||||||
|
| Export logs | [Workers Logpush](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/) |
|
||||||
|
| Attach request metadata for tracking and routing | [Custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/) |
|
||||||
|
| Override model costs | [Custom costs](https://developers.cloudflare.com/ai-gateway/configuration/custom-costs/) |
|
||||||
|
| Check supported providers, quotas, or pricing | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/), [limits](https://developers.cloudflare.com/ai-gateway/reference/limits/), and [pricing](https://developers.cloudflare.com/ai-gateway/reference/pricing/) |
|
||||||
|
|
||||||
|
Choose cache keys only for requests whose responses may safely be shared. Decide what prompt and response data may be stored before enabling logging; do not infer a provider's retention policy from gateway log settings.
|
||||||
|
|
||||||
|
For conditional policies and fallbacks, see [dynamic routing](./dynamic-routing.md).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AI Gateway SDK Integration
|
||||||
|
|
||||||
|
Choose an integration that matches the application's runtime and required API shape. Fetch its guide before installing packages or writing requests; model identifiers, SDK options, and gateway headers belong in the docs.
|
||||||
|
|
||||||
|
| Integration task | Current documentation |
|
||||||
|
|------------------|-----------------------|
|
||||||
|
| New single-model HTTP calls or OpenAI-compatible clients | [REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/) |
|
||||||
|
| Vercel AI SDK, provider adapters, and fallback providers | [Vercel AI SDK integration](https://developers.cloudflare.com/ai-gateway/integrations/vercel-ai-sdk/) |
|
||||||
|
| Preserve OpenAI-native endpoints with the OpenAI SDK, Python, or HTTP | [OpenAI provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/openai/) |
|
||||||
|
| Preserve Anthropic-native requests | [Anthropic provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/anthropic/) |
|
||||||
|
| Use another provider or a framework's configurable provider endpoint | [Provider guides](https://developers.cloudflare.com/ai-gateway/usage/providers/) — match the framework's expected API shape to the provider endpoint |
|
||||||
|
| Configure Workers AI and an AI binding | [Binding setup](https://developers.cloudflare.com/ai-gateway/integrations/aig-workers-ai-binding/) |
|
||||||
|
| Call Workers AI or third-party models from a Worker; use gateway methods | [Workers binding methods](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/) |
|
||||||
|
| Set request metadata, caching, or other gateway headers | [Header glossary](https://developers.cloudflare.com/ai-gateway/glossary/) |
|
||||||
|
| Invoke a dynamic route | [Dynamic route usage](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/) |
|
||||||
|
|
||||||
|
The [legacy Unified API](https://developers.cloudflare.com/ai-gateway/usage/chat-completion/) is deprecated for single-model calls, but dynamic routes still require its compatibility endpoint. Do not migrate a dynamic route to the REST inference endpoint as though it were a single-model call.
|
||||||
|
|
||||||
|
Confirm [gateway authentication and provider credentials](./configuration.md) separately, including the selected path's BYOK and billing behavior.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# AI Gateway Troubleshooting
|
||||||
|
|
||||||
|
Identify the request path and whether the failure comes from gateway access, upstream provider authentication, or request policy before changing credentials or retry behavior. A status code alone does not establish the failing layer.
|
||||||
|
|
||||||
|
| Symptom or task | Current documentation |
|
||||||
|
|-----------------|-----------------------|
|
||||||
|
| Authentication or provider errors, timeouts, DLP failures, or unexpected cache behavior | [Troubleshooting](https://developers.cloudflare.com/ai-gateway/reference/troubleshooting/) |
|
||||||
|
| Gateway authentication failure | [Authenticated Gateway](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) and [REST API authentication](https://developers.cloudflare.com/ai-gateway/usage/rest-api/#authentication) |
|
||||||
|
| Provider key or billing mismatch | [BYOK](https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/), [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/), and the [provider guide](https://developers.cloudflare.com/ai-gateway/usage/providers/) |
|
||||||
|
| Rate limits or repeated failures | [Rate limiting](https://developers.cloudflare.com/ai-gateway/features/rate-limiting/) and [request handling](https://developers.cloudflare.com/ai-gateway/configuration/request-handling/) |
|
||||||
|
| Unexpected cache hit or miss, including streaming behavior | [Caching](https://developers.cloudflare.com/ai-gateway/features/caching/) |
|
||||||
|
| Missing logs, collection overrides, or storage limits | [Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/) and [limits](https://developers.cloudflare.com/ai-gateway/reference/limits/) |
|
||||||
|
| Inspect headers, request metadata, usage, or export logs | [Header glossary](https://developers.cloudflare.com/ai-gateway/glossary/), [custom metadata](https://developers.cloudflare.com/ai-gateway/observability/custom-metadata/), [analytics](https://developers.cloudflare.com/ai-gateway/observability/analytics/), and [Logpush](https://developers.cloudflare.com/ai-gateway/observability/logging/logpush/) |
|
||||||
|
| Dynamic route failure | [Dynamic route usage](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/usage/) |
|
||||||
|
|
||||||
|
Check the existing SDK and gateway retry settings together before adding another retry loop. Follow [SDK integration](./sdk-integration.md) when an endpoint or model format is suspect.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Cloudflare AI Search
|
||||||
|
|
||||||
|
Use AI Search for managed content indexing and retrieval, with optional answer generation. Start with [How AI Search works](https://developers.cloudflare.com/ai-search/concepts/how-ai-search-works/).
|
||||||
|
|
||||||
|
## Choose the right product
|
||||||
|
|
||||||
|
- **Managed search or RAG over your content:** AI Search.
|
||||||
|
- **Custom embeddings and vector-index management:** [Vectorize](../vectorize/README.md).
|
||||||
|
- **Model inference without a managed retrieval pipeline:** [Workers AI](../workers-ai/README.md).
|
||||||
|
|
||||||
|
For freshness requirements, read [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/) for your data source before choosing an architecture. Use the current [limits and pricing](https://developers.cloudflare.com/ai-search/platform/limits-pricing/) instead of assuming a fixed indexing interval or account limit.
|
||||||
|
|
||||||
|
## Find the right documentation
|
||||||
|
|
||||||
|
Read the linked page before implementing; these references route to the maintained documentation instead of copying API examples or configuration.
|
||||||
|
|
||||||
|
| Task | Start here |
|
||||||
|
|------|------------|
|
||||||
|
| Build a new Worker integration | [Workers binding quick start](https://developers.cloudflare.com/ai-search/get-started/workers/) |
|
||||||
|
| Choose an API or maintain an existing integration | [API routes](api.md) |
|
||||||
|
| Connect data, configure indexing, or manage environments | [Configuration routes](configuration.md) |
|
||||||
|
| Choose retrieval, generation, or tenant isolation patterns | [Pattern routes](patterns.md) |
|
||||||
|
| Diagnose indexing, authentication, filters, or limits | [Troubleshooting routes](gotchas.md) |
|
||||||
|
|
||||||
|
Existing `env.AI.autorag()` integrations can continue to work. Use the [migration guide](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/) when upgrading; migration is not required just to maintain an existing integration.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# AI Search API Routes
|
||||||
|
|
||||||
|
Choose documentation that matches the integration you are working on. Fetch the reference before writing binding configuration, request types, response parsing, or streaming code.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| New Worker integration: bindings, search, chat completions, and streaming | [Search Workers binding](https://developers.cloudflare.com/ai-search/api/search/workers-binding/) |
|
||||||
|
| Create, list, configure, and inspect instances | [Instances Workers binding](https://developers.cloudflare.com/ai-search/api/instances/workers-binding/) |
|
||||||
|
| Query over HTTP and configure request authentication | [Search REST API](https://developers.cloudflare.com/ai-search/api/search/rest-api/) |
|
||||||
|
| Maintain an existing `env.AI.autorag()` integration | [Legacy Workers binding](https://developers.cloudflare.com/ai-search/api/migration/workers-binding-legacy/) |
|
||||||
|
| Upgrade a legacy binding, including responses, streaming, and filters | [Workers binding migration](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/) |
|
||||||
|
|
||||||
|
The legacy binding remains supported; use current bindings for new integrations. Keep legacy request and response handling together until deliberately migrating them.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# AI Search Configuration Routes
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Set up a Worker and local development | [Workers binding quick start](https://developers.cloudflare.com/ai-search/get-started/workers/) |
|
||||||
|
| Select built-in uploads, R2, or a website; check supported formats | [Data sources](https://developers.cloudflare.com/ai-search/configuration/data-source/) |
|
||||||
|
| Connect existing R2 content | [R2 data source](https://developers.cloudflare.com/ai-search/configuration/data-source/r2/) |
|
||||||
|
| Configure crawling and diagnose bot-protection requirements | [Website data source](https://developers.cloudflare.com/ai-search/configuration/data-source/website/) |
|
||||||
|
| Include or exclude files and URL paths | [Path filtering](https://developers.cloudflare.com/ai-search/configuration/indexing/path-filtering/) |
|
||||||
|
| Configure source syncs, trigger indexing, or pause and resume | [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/) |
|
||||||
|
| Grant AI Search access to R2 for indexing | [Service API token](https://developers.cloudflare.com/ai-search/configuration/indexing/service-api-token/) |
|
||||||
|
| Organize instances by application, tenant, or environment | [Namespaces](https://developers.cloudflare.com/ai-search/concepts/namespaces/) |
|
||||||
|
| Configure models | [Models](https://developers.cloudflare.com/ai-search/configuration/models/) |
|
||||||
|
| Inspect instance configuration and indexing progress | [Instances Workers binding](https://developers.cloudflare.com/ai-search/api/instances/workers-binding/) |
|
||||||
|
|
||||||
|
Indexing credentials and request authentication serve different purposes. For authenticating search requests over HTTP, use the [Search REST API](https://developers.cloudflare.com/ai-search/api/search/rest-api/) documentation.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# AI Search Troubleshooting Routes
|
||||||
|
|
||||||
|
| Symptom or question | Documentation |
|
||||||
|
|---------------------|---------------|
|
||||||
|
| API request fails, including authentication or missing instances | [API error codes](https://developers.cloudflare.com/ai-search/troubleshooting/api-error-codes/) |
|
||||||
|
| Upload or sync succeeds but content fails during processing | [Indexing error codes](https://developers.cloudflare.com/ai-search/troubleshooting/indexing-error-codes/) |
|
||||||
|
| Content is missing or stale | [Syncing](https://developers.cloudflare.com/ai-search/configuration/indexing/syncing/) and [supported data sources and formats](https://developers.cloudflare.com/ai-search/configuration/data-source/) |
|
||||||
|
| Filters return unexpected documents or no matches | [Filtering](https://developers.cloudflare.com/ai-search/configuration/retrieval/filtering/) and [metadata attributes](https://developers.cloudflare.com/ai-search/configuration/indexing/metadata/) |
|
||||||
|
| Thresholds exclude results or responses need tuning | [Result controls](https://developers.cloudflare.com/ai-search/configuration/retrieval/result-controls/) |
|
||||||
|
| Binding types, response parsing, or streaming fail after an upgrade | [Workers binding migration](https://developers.cloudflare.com/ai-search/api/migration/workers-binding/) |
|
||||||
|
| Capacity, file-size, or billing questions | [Limits and pricing](https://developers.cloudflare.com/ai-search/platform/limits-pricing/) |
|
||||||
|
|
||||||
|
For legacy binding behavior, start with [API routes](api.md). Do not apply current filter syntax or response shapes to legacy calls without following the migration guide.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# AI Search Pattern Routes
|
||||||
|
|
||||||
|
Choose retrieval-only search when your application displays chunks or handles generation itself; choose chat completions when AI Search should also generate the answer. Read [Search Workers binding](https://developers.cloudflare.com/ai-search/api/search/workers-binding/) for both paths and streaming behavior.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Isolate tenants using separate instances or a shared filtered instance | [Multitenancy](https://developers.cloudflare.com/ai-search/how-to/per-tenant-search/) |
|
||||||
|
| Define built-in or custom metadata | [Metadata attributes](https://developers.cloudflare.com/ai-search/configuration/indexing/metadata/) |
|
||||||
|
| Filter by metadata, combine conditions, or match a folder and subfolders | [Filtering](https://developers.cloudflare.com/ai-search/configuration/retrieval/filtering/) |
|
||||||
|
| Tune result count and relevance thresholds | [Result controls](https://developers.cloudflare.com/ai-search/configuration/retrieval/result-controls/) |
|
||||||
|
| Resolve follow-up queries using conversation context | [Query rewriting](https://developers.cloudflare.com/ai-search/configuration/retrieval/query-rewriting/) |
|
||||||
|
| Improve result ordering with a second model | [Reranking](https://developers.cloudflare.com/ai-search/configuration/retrieval/reranking/) |
|
||||||
|
| Customize generation and query-rewriting instructions | [System prompt](https://developers.cloudflare.com/ai-search/configuration/retrieval/system-prompt/) |
|
||||||
|
|
||||||
|
For tenant isolation, read the full multitenancy guide before choosing an approach. A lower-bound folder comparison alone does not establish a tenant boundary; use the documented filtering semantics.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Cloudflare Workers Analytics Engine Reference
|
||||||
|
|
||||||
|
Expert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine.
|
||||||
|
|
||||||
|
## What is Analytics Engine?
|
||||||
|
|
||||||
|
Time-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for:
|
||||||
|
- Custom user-facing analytics dashboards
|
||||||
|
- Usage-based billing & metering
|
||||||
|
- Per-customer/per-feature monitoring
|
||||||
|
- High-frequency instrumentation without performance impact
|
||||||
|
|
||||||
|
**Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
| Concept | Description | Example |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| **Dataset** | Logical table for related metrics | `api_requests`, `user_events` |
|
||||||
|
| **Data Point** | Single measurement with timestamp | One API request's metrics |
|
||||||
|
| **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id |
|
||||||
|
| **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes |
|
||||||
|
| **Indexes** | Filtered blobs for efficient queries | customer_id, api_key |
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Start Here | Then Read |
|
||||||
|
|------|------------|-----------|
|
||||||
|
| **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | |
|
||||||
|
| **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | |
|
||||||
|
| **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | |
|
||||||
|
| **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | |
|
||||||
|
| **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | |
|
||||||
|
|
||||||
|
## When to Use Analytics Engine
|
||||||
|
|
||||||
|
```
|
||||||
|
Need to track metrics? → Yes
|
||||||
|
↓
|
||||||
|
Millions of unique dimension values? → Yes
|
||||||
|
↓
|
||||||
|
Need real-time queries? → Yes
|
||||||
|
↓
|
||||||
|
Use Analytics Engine ✓
|
||||||
|
|
||||||
|
Alternative scenarios:
|
||||||
|
- Low cardinality (<10k unique values) → Workers Analytics (free tier)
|
||||||
|
- Complex joins/relations → D1 Database
|
||||||
|
- Logs/debugging → Tail Workers (logpush)
|
||||||
|
- External tools → Send to external analytics (Datadog, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Add binding to `wrangler.jsonc`:
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"analytics_engine_datasets": [
|
||||||
|
{ "binding": "ANALYTICS", "dataset": "my_events" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Write data points (fire-and-forget, no await):
|
||||||
|
```typescript
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: ["/api/users", "GET", "200"],
|
||||||
|
doubles: [145.2, 1], // latency_ms, count
|
||||||
|
indexes: [customerId]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Query via SQL API (HTTP):
|
||||||
|
```sql
|
||||||
|
SELECT blob1, SUM(double2) AS total_requests
|
||||||
|
FROM my_events
|
||||||
|
WHERE index1 = 'customer_123'
|
||||||
|
AND timestamp >= NOW() - INTERVAL '7' DAY
|
||||||
|
GROUP BY blob1
|
||||||
|
ORDER BY total_requests DESC
|
||||||
|
```
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits
|
||||||
|
- **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax
|
||||||
|
- **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns
|
||||||
|
- **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)
|
||||||
|
- [GraphQL Analytics API Reference](../graphql-api/) - Query built-in Cloudflare analytics (HTTP, Workers, DNS, Firewall, etc.)
|
||||||
|
- [Observability Reference](../observability/) - Workers Logs, Traces, and real-time debugging
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Analytics Engine API Reference
|
||||||
|
|
||||||
|
## Writing Data
|
||||||
|
|
||||||
|
### `writeDataPoint()`
|
||||||
|
|
||||||
|
Fire-and-forget (returns `void`, not Promise). Writes happen asynchronously.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AnalyticsEngineDataPoint {
|
||||||
|
blobs?: string[]; // Up to 20 strings (dimensions), 16KB each
|
||||||
|
doubles?: number[]; // Up to 20 numbers (metrics)
|
||||||
|
indexes?: string[]; // 1 indexed string for high-cardinality filtering
|
||||||
|
}
|
||||||
|
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: ["/api/users", "GET", "200"],
|
||||||
|
doubles: [145.2, 1], // latency_ms, count
|
||||||
|
indexes: ["customer_abc123"]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped.
|
||||||
|
|
||||||
|
**Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique).
|
||||||
|
|
||||||
|
### Full Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const start = Date.now();
|
||||||
|
const url = new URL(request.url);
|
||||||
|
try {
|
||||||
|
const response = await handleRequest(request);
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [url.pathname, request.method, response.status.toString()],
|
||||||
|
doubles: [Date.now() - start, 1],
|
||||||
|
indexes: [request.headers.get("x-api-key") || "anonymous"]
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [url.pathname, request.method, "500"],
|
||||||
|
doubles: [Date.now() - start, 1, 0],
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQL API (External Only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d "SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Column References
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- blob1..blob20, double1..double20, index1, timestamp
|
||||||
|
SELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests
|
||||||
|
FROM my_dataset
|
||||||
|
WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY
|
||||||
|
GROUP BY blob1
|
||||||
|
HAVING COUNT(*) > 100
|
||||||
|
ORDER BY requests DESC LIMIT 100
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()`
|
||||||
|
|
||||||
|
**Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'`
|
||||||
|
|
||||||
|
### Query Examples
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Top endpoints
|
||||||
|
SELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency
|
||||||
|
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR
|
||||||
|
GROUP BY blob1 ORDER BY requests DESC LIMIT 20
|
||||||
|
|
||||||
|
-- Error rate
|
||||||
|
SELECT blob1, COUNT(*) AS total,
|
||||||
|
SUM(if(blob3 LIKE '5%', 1, 0)) AS errors
|
||||||
|
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR
|
||||||
|
GROUP BY blob1 HAVING total > 50
|
||||||
|
|
||||||
|
-- P95 latency
|
||||||
|
SELECT blob1, quantile(0.95)(double1) AS p95
|
||||||
|
FROM api_requests GROUP BY blob1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"data": [{"endpoint": "/api/users", "requests": 1523}], "rows": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Resource | Limit |
|
||||||
|
|----------|-------|
|
||||||
|
| Blobs/Doubles per point | 20 each |
|
||||||
|
| Indexes per point | 1 |
|
||||||
|
| Blob/Index size | 16KB |
|
||||||
|
| Data retention | 90 days |
|
||||||
|
| Query timeout | 30s |
|
||||||
|
|
||||||
|
**Critical:** High write volumes (>1M/min) trigger automatic sampling.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Analytics Engine Configuration
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Add binding to `wrangler.jsonc`
|
||||||
|
2. Deploy Worker
|
||||||
|
3. Dataset created automatically on first write
|
||||||
|
4. Query via SQL API
|
||||||
|
|
||||||
|
## wrangler.jsonc
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"name": "my-worker",
|
||||||
|
"analytics_engine_datasets": [
|
||||||
|
{ "binding": "ANALYTICS", "dataset": "my_events" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple datasets for separate concerns:
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"analytics_engine_datasets": [
|
||||||
|
{ "binding": "API_ANALYTICS", "dataset": "api_requests" },
|
||||||
|
{ "binding": "USER_EVENTS", "dataset": "user_activity" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Env {
|
||||||
|
ANALYTICS: AnalyticsEngineDataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env) {
|
||||||
|
// No await - returns void, fire-and-forget
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [pathname, method, status], // String dimensions (max 20)
|
||||||
|
doubles: [latency, 1], // Numeric metrics (max 20)
|
||||||
|
indexes: [apiKey] // High-cardinality filter (max 1)
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Point Limits
|
||||||
|
|
||||||
|
| Field | Limit | SQL Access |
|
||||||
|
|-------|-------|------------|
|
||||||
|
| blobs | 20 strings, 16KB each | `blob1`...`blob20` |
|
||||||
|
| doubles | 20 numbers | `double1`...`double20` |
|
||||||
|
| indexes | 1 string, 16KB | `index1` |
|
||||||
|
|
||||||
|
## Write Behavior
|
||||||
|
|
||||||
|
| Scenario | Behavior |
|
||||||
|
|----------|----------|
|
||||||
|
| <1M writes/min | All accepted |
|
||||||
|
| >1M writes/min | Automatic sampling |
|
||||||
|
| Invalid data | Silent failure (check tail logs) |
|
||||||
|
|
||||||
|
**Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics.
|
||||||
|
|
||||||
|
## Query Limits
|
||||||
|
|
||||||
|
| Resource | Limit |
|
||||||
|
|----------|-------|
|
||||||
|
| Query timeout | 30 seconds |
|
||||||
|
| Data retention | 90 days (default) |
|
||||||
|
| Result size | ~10MB |
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
**Free tier:** 10M writes/month, 1M reads/month
|
||||||
|
|
||||||
|
**Paid:** $0.05 per 1M writes, $1.00 per 1M reads
|
||||||
|
|
||||||
|
## Environment-Specific
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"analytics_engine_datasets": [
|
||||||
|
{ "binding": "ANALYTICS", "dataset": "prod_events" }
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"staging": {
|
||||||
|
"analytics_engine_datasets": [
|
||||||
|
{ "binding": "ANALYTICS", "dataset": "staging_events" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler tail # Check for sampling/write errors
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check write activity
|
||||||
|
SELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes
|
||||||
|
FROM my_dataset
|
||||||
|
WHERE timestamp >= NOW() - INTERVAL '24' HOUR
|
||||||
|
GROUP BY hour
|
||||||
|
```
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Analytics Engine Gotchas
|
||||||
|
|
||||||
|
## Critical Issues
|
||||||
|
|
||||||
|
### Sampling at High Volumes
|
||||||
|
|
||||||
|
**Problem:** Queries return fewer points than written at >1M writes/min.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```typescript
|
||||||
|
// Pre-aggregate before writing
|
||||||
|
let buffer = { count: 0, total: 0 };
|
||||||
|
buffer.count++; buffer.total += value;
|
||||||
|
|
||||||
|
// Write once per second instead of per request
|
||||||
|
if (Date.now() % 1000 === 0) {
|
||||||
|
env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detection:** `npx wrangler tail` → look for "sampling enabled"
|
||||||
|
|
||||||
|
### writeDataPoint Returns void
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Pointless await
|
||||||
|
await env.ANALYTICS.writeDataPoint({...});
|
||||||
|
|
||||||
|
// ✅ Fire-and-forget
|
||||||
|
env.ANALYTICS.writeDataPoint({...});
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes can fail silently. Check tail logs.
|
||||||
|
|
||||||
|
### Index vs Blob
|
||||||
|
|
||||||
|
| Cardinality | Use | Example |
|
||||||
|
|-------------|-----|---------|
|
||||||
|
| Millions | **Index** | user_id, api_key |
|
||||||
|
| Hundreds | **Blob** | endpoint, status_code, country |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct
|
||||||
|
{ blobs: [method, path, status], indexes: [userId] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Query from Workers
|
||||||
|
|
||||||
|
Query API requires HTTP auth. Use external service or cache in KV/D1.
|
||||||
|
|
||||||
|
### No Custom Timestamps
|
||||||
|
|
||||||
|
Auto-generated at write time. Store original in blob if needed.
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
| Error | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| Binding not found | Check wrangler.jsonc, redeploy |
|
||||||
|
| No data in query | Wait 30s; check dataset name; check time range |
|
||||||
|
| Query timeout | Add time filter; use index for filtering |
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Resource | Limit |
|
||||||
|
|----------|-------|
|
||||||
|
| Blobs per point | 20 |
|
||||||
|
| Doubles per point | 20 |
|
||||||
|
| Indexes per point | 1 |
|
||||||
|
| Blob/Index size | 16KB |
|
||||||
|
| Write rate (no sampling) | ~1M/min |
|
||||||
|
| Retention | 90 days |
|
||||||
|
| Query timeout | 30s |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
✅ Pre-aggregate at high volumes
|
||||||
|
✅ Use index for high-cardinality (millions)
|
||||||
|
✅ Always include time filter in queries
|
||||||
|
✅ Design schema before coding
|
||||||
|
|
||||||
|
❌ Don't await writeDataPoint
|
||||||
|
❌ Don't use index for low-cardinality
|
||||||
|
❌ Don't query without time range
|
||||||
|
❌ Don't assume all writes succeed
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Analytics Engine Patterns
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
| Use Case | Key Metrics | Index On |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| API Metering | requests, bytes, compute_units | api_key |
|
||||||
|
| Feature Usage | feature, action, duration | user_id |
|
||||||
|
| Error Tracking | error_type, endpoint, count | customer_id |
|
||||||
|
| Performance | latency_ms, cache_status | endpoint |
|
||||||
|
| A/B Testing | variant, conversions | user_id |
|
||||||
|
|
||||||
|
## API Metering (Billing)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [pathname, method, status, tier],
|
||||||
|
doubles: [1, computeUnits, bytes, latencyMs],
|
||||||
|
indexes: [apiKey]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query: Monthly usage by customer
|
||||||
|
// SELECT index1 AS api_key, SUM(double2) AS compute_units
|
||||||
|
// FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)],
|
||||||
|
doubles: [1, timeToErrorMs],
|
||||||
|
indexes: [customerId]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Monitoring
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
env.ANALYTICS.writeDataPoint({
|
||||||
|
blobs: [pathname, method, cacheStatus, status],
|
||||||
|
doubles: [latencyMs, 1],
|
||||||
|
indexes: [userId]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query: P95 latency by endpoint
|
||||||
|
// SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| ❌ Wrong | ✅ Correct |
|
||||||
|
|----------|-----------|
|
||||||
|
| `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) |
|
||||||
|
| `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` |
|
||||||
|
| `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV |
|
||||||
|
| Write every request at 10M/min | Pre-aggregate per second |
|
||||||
|
| Query from Worker | Query from external service/API |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Design schema upfront** - Document blob/double/index assignments
|
||||||
|
2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations
|
||||||
|
3. **Use enums for blobs** - Consistent values like `Status.SUCCESS`
|
||||||
|
4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count))
|
||||||
|
5. **Test queries early** - Validate schema before heavy writes
|
||||||
|
|
||||||
|
## Schema Template
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Dataset: my_metrics
|
||||||
|
*
|
||||||
|
* Blobs:
|
||||||
|
* blob1: endpoint, blob2: method, blob3: status
|
||||||
|
*
|
||||||
|
* Doubles:
|
||||||
|
* double1: latency_ms, double2: count (always 1)
|
||||||
|
*
|
||||||
|
* Indexes:
|
||||||
|
* index1: customer_id (high cardinality)
|
||||||
|
*/
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Cloudflare API Shield Reference
|
||||||
|
|
||||||
|
Expert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring.
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Files to Read |
|
||||||
|
|------|---------------|
|
||||||
|
| Initial setup | README → configuration.md |
|
||||||
|
| Implement JWT validation | configuration.md → api.md |
|
||||||
|
| Add schema validation | configuration.md → patterns.md |
|
||||||
|
| Detect API attacks | patterns.md → api.md |
|
||||||
|
| Debug issues | gotchas.md |
|
||||||
|
|
||||||
|
## Feature Selection
|
||||||
|
|
||||||
|
What protection do you need?
|
||||||
|
|
||||||
|
```
|
||||||
|
├─ Validate request/response structure → Schema Validation 2.0 (configuration.md)
|
||||||
|
├─ Verify auth tokens → JWT Validation (configuration.md)
|
||||||
|
├─ Client certificates → mTLS (configuration.md)
|
||||||
|
├─ Detect BOLA attacks → BOLA Detection (patterns.md)
|
||||||
|
├─ Track auth coverage → Auth Posture (patterns.md)
|
||||||
|
├─ Stop volumetric abuse → Abuse Detection (patterns.md)
|
||||||
|
└─ Discover shadow APIs → API Discovery (api.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs
|
||||||
|
- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations
|
||||||
|
- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows
|
||||||
|
- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
API Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
|
||||||
|
- [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/)
|
||||||
|
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
Base: `/zones/{zone_id}/api_gateway`
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /operations # List
|
||||||
|
GET /operations/{op_id} # Get single
|
||||||
|
POST /operations/item # Create: {endpoint,host,method}
|
||||||
|
POST /operations # Bulk: {operations:[{endpoint,host,method}]}
|
||||||
|
DELETE /operations/{op_id} # Delete
|
||||||
|
DELETE /operations # Bulk delete: {operation_ids:[...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /discovery/operations # List discovered
|
||||||
|
PATCH /discovery/operations/{op_id} # Update: {state:"saved"|"ignored"}
|
||||||
|
PATCH /discovery/operations # Bulk: {operation_ids:{id:{state}}}
|
||||||
|
GET /discovery # OpenAPI export
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /configuration # Get session ID config
|
||||||
|
PUT /configuration # Update: {auth_id_characteristics:[{name,type:"header"|"cookie"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /token_validation # List
|
||||||
|
POST /token_validation # Create: {name,location:{header:"..."},jwks:"..."}
|
||||||
|
POST /jwt_validation_rules # Rule: {name,hostname,token_validation_id,action:"block"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workers Integration
|
||||||
|
|
||||||
|
### Access JWT Claims
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
async fetch(req, env) {
|
||||||
|
// Access validated JWT payload
|
||||||
|
const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0];
|
||||||
|
if (jwt) {
|
||||||
|
const userId = jwt.sub;
|
||||||
|
const role = jwt.role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access mTLS Info
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
async fetch(req, env) {
|
||||||
|
const tls = req.cf?.tlsClientAuth;
|
||||||
|
if (tls?.certVerified === 'SUCCESS') {
|
||||||
|
const fingerprint = tls.certFingerprintSHA256;
|
||||||
|
// Authenticated client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic JWKS Update
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
async scheduled(event, env) {
|
||||||
|
const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json();
|
||||||
|
await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({jwks: JSON.stringify(jwks)})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firewall Fields
|
||||||
|
|
||||||
|
### Core Fields
|
||||||
|
```js
|
||||||
|
cf.api_gateway.auth_id_present // Session ID present
|
||||||
|
cf.api_gateway.request_violates_schema // Schema violation
|
||||||
|
cf.api_gateway.fallthrough_triggered // No endpoint match
|
||||||
|
cf.tls_client_auth.cert_verified // mTLS cert valid
|
||||||
|
cf.tls_client_auth.cert_fingerprint_sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT Validation (2026)
|
||||||
|
```js
|
||||||
|
// Modern validation syntax
|
||||||
|
is_jwt_valid(http.request.jwt.payload["{config_id}"][0])
|
||||||
|
|
||||||
|
// Legacy (still supported)
|
||||||
|
cf.api_gateway.jwt_claims_valid
|
||||||
|
|
||||||
|
// Extract claims
|
||||||
|
lookup_json_string(http.request.jwt.payload["{config_id}"][0], "claim_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risk Labels (2026)
|
||||||
|
```js
|
||||||
|
// BOLA detection
|
||||||
|
cf.api_gateway.cf-risk-bola-enumeration // Sequential resource access detected
|
||||||
|
cf.api_gateway.cf-risk-bola-pollution // Parameter pollution detected
|
||||||
|
|
||||||
|
// Authentication posture
|
||||||
|
cf.api_gateway.cf-risk-missing-auth // Endpoint lacks authentication
|
||||||
|
cf.api_gateway.cf-risk-mixed-auth // Inconsistent auth patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
## BOLA Detection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /user_schemas/{schema_id}/bola # Get BOLA config
|
||||||
|
PATCH /user_schemas/{schema_id}/bola # Update: {enabled:true}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth Posture
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /discovery/authentication_posture # List unprotected endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## GraphQL Protection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /settings/graphql_protection # Get limits
|
||||||
|
PUT /settings/graphql_protection # Set: {max_depth,max_size}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [configuration.md](configuration.md) - Setup guides for all features
|
||||||
|
- [patterns.md](patterns.md) - Firewall rules and common patterns
|
||||||
|
- [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/)
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Schema Validation 2.0 Setup
|
||||||
|
|
||||||
|
> ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0.
|
||||||
|
|
||||||
|
**Upload schema (Dashboard):**
|
||||||
|
```
|
||||||
|
Security > API Shield > Schema Validation > Add validation
|
||||||
|
- Upload .yml/.yaml/.json (OpenAPI v3.0)
|
||||||
|
- Endpoints auto-added to Endpoint Management
|
||||||
|
- Action: Log | Block | None
|
||||||
|
- Body inspection: JSON payloads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change validation action:**
|
||||||
|
```
|
||||||
|
Security > API Shield > Settings > Schema Validation
|
||||||
|
Per-endpoint: Filter → ellipses → Change action
|
||||||
|
Default action: Set global mitigation action
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration from Classic:**
|
||||||
|
```
|
||||||
|
1. Export existing schema (if available)
|
||||||
|
2. Delete all Classic schema validation rules
|
||||||
|
3. Wait 5 min for cache clear
|
||||||
|
4. Re-upload via Schema Validation 2.0 interface
|
||||||
|
5. Verify in Security > Events
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallthrough rule** (catch-all unknown endpoints):
|
||||||
|
```
|
||||||
|
Security > API Shield > Settings > Fallthrough > Use Template
|
||||||
|
- Select hostnames
|
||||||
|
- Create rule with cf.api_gateway.fallthrough_triggered
|
||||||
|
- Action: Log (discover) or Block (strict)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses.
|
||||||
|
|
||||||
|
## JWT Validation
|
||||||
|
|
||||||
|
**Setup token config:**
|
||||||
|
```
|
||||||
|
Security > API Shield > Settings > JWT Settings > Add configuration
|
||||||
|
- Name: "Auth0 JWT Config"
|
||||||
|
- Location: Header/Cookie + name (e.g., "Authorization")
|
||||||
|
- JWKS: Paste public keys from IdP
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create validation rule:**
|
||||||
|
```
|
||||||
|
Security > API Shield > API Rules > Add rule
|
||||||
|
- Hostname: api.example.com
|
||||||
|
- Deselect endpoints to ignore
|
||||||
|
- Token config: Select config
|
||||||
|
- Enforce presence: Ignore or Mark as non-compliant
|
||||||
|
- Action: Log/Block/Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rate limit by JWT claim:**
|
||||||
|
```wirefilter
|
||||||
|
lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special cases:**
|
||||||
|
- Two JWTs, different IdPs: Create 2 configs, select both, "Validate all"
|
||||||
|
- IdP migration: 2 configs + 2 rules, adjust actions per state
|
||||||
|
- Bearer prefix: API Shield handles with/without
|
||||||
|
- Nested claims: Dot notation `user.email`
|
||||||
|
|
||||||
|
## Mutual TLS (mTLS)
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
```
|
||||||
|
SSL/TLS > Client Certificates > Create Certificate
|
||||||
|
- Generate CF-managed CA (all plans)
|
||||||
|
- Upload custom CA (Enterprise, max 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configure mTLS rule:**
|
||||||
|
```
|
||||||
|
Security > API Shield > mTLS
|
||||||
|
- Select hostname(s)
|
||||||
|
- Choose certificate(s)
|
||||||
|
- Action: Block/Log/Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365
|
||||||
|
curl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Identifiers
|
||||||
|
|
||||||
|
Critical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users.
|
||||||
|
|
||||||
|
**Examples:** JWT sub claim, session token, API key, custom user ID header
|
||||||
|
|
||||||
|
**Configure:**
|
||||||
|
```
|
||||||
|
Security > API Shield > Settings > Session Identifiers
|
||||||
|
- Type: Header/Cookie
|
||||||
|
- Name: "X-User-ID" or "Authorization"
|
||||||
|
```
|
||||||
|
|
||||||
|
## BOLA Detection
|
||||||
|
|
||||||
|
Detects Broken Object Level Authorization attacks (enumeration + parameter pollution).
|
||||||
|
|
||||||
|
**Enable:**
|
||||||
|
```
|
||||||
|
Security > API Shield > Schema Validation > [Select Schema] > BOLA Detection
|
||||||
|
- Enable detection
|
||||||
|
- Threshold: Sensitivity level (Low/Medium/High)
|
||||||
|
- Action: Log or Block
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Schema Validation 2.0 enabled
|
||||||
|
- Session identifiers configured
|
||||||
|
- Minimum traffic: 1000+ requests/day per endpoint
|
||||||
|
|
||||||
|
## Authentication Posture
|
||||||
|
|
||||||
|
Identifies unprotected or inconsistently protected endpoints.
|
||||||
|
|
||||||
|
**View report:**
|
||||||
|
```
|
||||||
|
Security > API Shield > Authentication Posture
|
||||||
|
- Shows endpoints lacking JWT/mTLS
|
||||||
|
- Highlights mixed authentication patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remediate:**
|
||||||
|
1. Review flagged endpoints
|
||||||
|
2. Add JWT validation rules
|
||||||
|
3. Configure mTLS for sensitive endpoints
|
||||||
|
4. Monitor posture score
|
||||||
|
|
||||||
|
## Volumetric Abuse + GraphQL
|
||||||
|
|
||||||
|
**Volumetric Abuse Detection:**
|
||||||
|
`Security > API Shield > Settings > Volumetric Abuse Detection`
|
||||||
|
- Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block
|
||||||
|
|
||||||
|
**GraphQL Protection:**
|
||||||
|
`Security > API Shield > Settings > GraphQL Protection`
|
||||||
|
- Max query depth: 10, max size: 100KB, block introspection (production)
|
||||||
|
|
||||||
|
## Terraform
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Session identifier
|
||||||
|
resource "cloudflare_api_shield" "main" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
auth_id_characteristics {
|
||||||
|
type = "header"
|
||||||
|
name = "Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add endpoint
|
||||||
|
resource "cloudflare_api_shield_operation" "users_get" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
method = "GET"
|
||||||
|
host = "api.example.com"
|
||||||
|
endpoint = "/api/users/{id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# JWT validation rule
|
||||||
|
resource "cloudflare_ruleset" "jwt_validation" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
name = "API JWT Validation"
|
||||||
|
kind = "zone"
|
||||||
|
phase = "http_request_firewall_custom"
|
||||||
|
|
||||||
|
rules {
|
||||||
|
action = "block"
|
||||||
|
expression = "(http.host eq \"api.example.com\" and not is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))"
|
||||||
|
description = "Block invalid JWTs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [api.md](api.md) - API endpoints and Workers integration
|
||||||
|
- [patterns.md](patterns.md) - Firewall rules and deployment patterns
|
||||||
|
- [gotchas.md](gotchas.md) - Troubleshooting and limits
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Gotchas & Troubleshooting
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### "Schema Validation 2.0 not working after migration"
|
||||||
|
|
||||||
|
**Cause:** Classic rules still active, conflicting with new system
|
||||||
|
**Solution:**
|
||||||
|
1. Delete ALL Classic schema validation rules
|
||||||
|
2. Clear Cloudflare cache (wait 5 min)
|
||||||
|
3. Re-upload schema via new Schema Validation 2.0 interface
|
||||||
|
4. Verify in Security > Events
|
||||||
|
5. Check action is set (Log/Block)
|
||||||
|
|
||||||
|
### "Schema validation blocking valid requests"
|
||||||
|
|
||||||
|
**Cause:** Schema too restrictive, missing fields, or incorrect types
|
||||||
|
**Solution:**
|
||||||
|
1. Check Firewall Events for violation details
|
||||||
|
2. Review schema in Settings
|
||||||
|
3. Test schema in Swagger Editor
|
||||||
|
4. Use Log mode to validate before blocking
|
||||||
|
5. Update schema with correct specifications
|
||||||
|
6. Ensure Schema Validation 2.0 (not Classic)
|
||||||
|
|
||||||
|
### "JWT validation failing"
|
||||||
|
|
||||||
|
**Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew
|
||||||
|
**Solution:**
|
||||||
|
1. Verify JWKS matches IdP configuration
|
||||||
|
2. Check token `exp` claim is valid
|
||||||
|
3. Confirm header/cookie name matches config
|
||||||
|
4. Test token at jwt.io
|
||||||
|
5. Account for clock skew (±5 min tolerance)
|
||||||
|
6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload["{config_id}"][0])`
|
||||||
|
|
||||||
|
### "BOLA detection false positives"
|
||||||
|
|
||||||
|
**Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high
|
||||||
|
**Solution:**
|
||||||
|
1. Review BOLA events in Security > Events
|
||||||
|
2. Lower sensitivity threshold (High → Medium → Low)
|
||||||
|
3. Exclude legitimate bulk operations from detection
|
||||||
|
4. Ensure session identifiers uniquely identify users
|
||||||
|
5. Verify minimum traffic requirements met (1000+ req/day)
|
||||||
|
|
||||||
|
### "Risk labels not appearing in firewall rules"
|
||||||
|
|
||||||
|
**Cause:** Feature not enabled, insufficient traffic, or missing session identifiers
|
||||||
|
**Solution:**
|
||||||
|
1. Verify Schema Validation 2.0 enabled
|
||||||
|
2. Enable BOLA Detection in schema settings
|
||||||
|
3. Configure session identifiers (required for BOLA)
|
||||||
|
4. Wait 24-48h for ML model training
|
||||||
|
5. Check minimum traffic thresholds met
|
||||||
|
|
||||||
|
### "Endpoint discovery not finding APIs"
|
||||||
|
|
||||||
|
**Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config
|
||||||
|
**Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily.
|
||||||
|
|
||||||
|
### "Sequence detection false positives"
|
||||||
|
|
||||||
|
**Cause:** Lookback window issues, non-unique session IDs, or model sensitivity
|
||||||
|
**Solution:**
|
||||||
|
1. Review lookback settings (10 reqs to managed endpoints, 10min window)
|
||||||
|
2. Ensure session ID uniqueness per user (not shared tokens)
|
||||||
|
3. Adjust positive/negative model balance
|
||||||
|
4. Exclude legitimate workflows from detection
|
||||||
|
|
||||||
|
### "GraphQL protection blocking valid queries"
|
||||||
|
|
||||||
|
**Cause:** Query depth/size limits too restrictive, complex but legitimate queries
|
||||||
|
**Solution:**
|
||||||
|
1. Review blocked query patterns in Security > Events
|
||||||
|
2. Increase max_depth (default: 10) if needed
|
||||||
|
3. Increase max_size (default: 100KB) for complex queries
|
||||||
|
4. Whitelist specific query signatures
|
||||||
|
5. Use Log mode to tune before blocking
|
||||||
|
|
||||||
|
### "Token invalid"
|
||||||
|
|
||||||
|
**Cause:** Configuration error, JWKS mismatch, or expired token
|
||||||
|
**Solution:** Verify config matches IdP, update JWKS, check token expiration
|
||||||
|
|
||||||
|
### "Schema violation"
|
||||||
|
|
||||||
|
**Cause:** Missing required fields, wrong data types, or spec mismatch
|
||||||
|
**Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec
|
||||||
|
|
||||||
|
### "Fallthrough"
|
||||||
|
|
||||||
|
**Cause:** Unknown endpoint or pattern mismatch
|
||||||
|
**Solution:** Update schema with all endpoints, check path pattern matching
|
||||||
|
|
||||||
|
### "mTLS failed"
|
||||||
|
|
||||||
|
**Cause:** Certificate untrusted/expired or wrong CA
|
||||||
|
**Solution:** Verify cert chain, check expiration, confirm correct CA uploaded
|
||||||
|
|
||||||
|
## Limits (2026)
|
||||||
|
|
||||||
|
| Resource/Limit | Value | Notes |
|
||||||
|
|----------------|-------|-------|
|
||||||
|
| OpenAPI version | v3.0.x only | No external refs, must be valid |
|
||||||
|
| Schema operations | 10K (Enterprise) | Contact for higher limits |
|
||||||
|
| JWT validation sources | Headers/cookies only | No query params/body |
|
||||||
|
| Endpoint discovery | 500+ reqs/10d | Minimum for ML model |
|
||||||
|
| Path normalization | Automatic | `/profile/238` → `/profile/{var1}` |
|
||||||
|
| Schema parameters | No `content` field | No object param validation |
|
||||||
|
| BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum |
|
||||||
|
| Session ID uniqueness | Required | BOLA/Sequence need unique IDs |
|
||||||
|
| GraphQL max depth | 1-50 | Default: 10 |
|
||||||
|
| GraphQL max size | 1KB-1MB | Default: 100KB |
|
||||||
|
| JWT claim nesting | 10 levels max | Use dot notation |
|
||||||
|
| mTLS CA certificates | 5 custom max | CF-managed unlimited |
|
||||||
|
| Schema upload size | 5MB max | Compressed OpenAPI spec |
|
||||||
|
| Volumetric abuse baseline | 7 days training | Initial ML period |
|
||||||
|
| Auth Posture refresh | Daily | Updated nightly |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [configuration.md](configuration.md) - Setup guides to avoid common issues
|
||||||
|
- [patterns.md](patterns.md) - Best practices and progressive rollout
|
||||||
|
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# Patterns & Use Cases
|
||||||
|
|
||||||
|
## Protect API with Schema + JWT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Upload OpenAPI schema
|
||||||
|
POST /zones/{zone_id}/api_gateway/user_schemas
|
||||||
|
|
||||||
|
# 2. Configure JWT validation
|
||||||
|
POST /zones/{zone_id}/api_gateway/token_validation
|
||||||
|
{
|
||||||
|
"name": "Auth0",
|
||||||
|
"location": {"header": "Authorization"},
|
||||||
|
"jwks": "{...}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Create JWT rule
|
||||||
|
POST /zones/{zone_id}/api_gateway/jwt_validation_rules
|
||||||
|
|
||||||
|
# 4. Set schema validation action
|
||||||
|
PUT /zones/{zone_id}/api_gateway/settings/schema_validation
|
||||||
|
{"validation_default_mitigation_action": "block"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progressive Rollout
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Log mode: Observe false positives
|
||||||
|
- Schema: Action = Log
|
||||||
|
- JWT: Action = Log
|
||||||
|
|
||||||
|
2. Block subset: Protect critical endpoints
|
||||||
|
- Change specific endpoint actions to Block
|
||||||
|
- Monitor firewall events
|
||||||
|
|
||||||
|
3. Full enforcement: Block all violations
|
||||||
|
- Change default action to Block
|
||||||
|
- Handle fallthrough with custom rule
|
||||||
|
```
|
||||||
|
|
||||||
|
## BOLA Detection
|
||||||
|
|
||||||
|
### Enumeration Detection
|
||||||
|
Detects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Block BOLA enumeration attempts
|
||||||
|
(cf.api_gateway.cf-risk-bola-enumeration and http.host eq "api.example.com")
|
||||||
|
// Action: Block or Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameter Pollution
|
||||||
|
Detects duplicate/excessive parameters in requests.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Block parameter pollution
|
||||||
|
(cf.api_gateway.cf-risk-bola-pollution and http.host eq "api.example.com")
|
||||||
|
// Action: Block
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined BOLA Protection
|
||||||
|
```javascript
|
||||||
|
// Comprehensive BOLA rule
|
||||||
|
(cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution)
|
||||||
|
and http.host eq "api.example.com"
|
||||||
|
// Action: Block
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Posture
|
||||||
|
|
||||||
|
### Detect Missing Auth
|
||||||
|
```javascript
|
||||||
|
// Log endpoints lacking authentication
|
||||||
|
(cf.api_gateway.cf-risk-missing-auth and http.host eq "api.example.com")
|
||||||
|
// Action: Log (for audit)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detect Mixed Auth
|
||||||
|
```javascript
|
||||||
|
// Alert on inconsistent auth patterns
|
||||||
|
(cf.api_gateway.cf-risk-mixed-auth and http.host eq "api.example.com")
|
||||||
|
// Action: Log (review required)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fallthrough Detection (Shadow APIs)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// WAF Custom Rule
|
||||||
|
(cf.api_gateway.fallthrough_triggered and http.host eq "api.example.com")
|
||||||
|
// Action: Log (discover unknown) or Block (strict)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting by User
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Rate Limiting Rule (modern syntax)
|
||||||
|
(http.host eq "api.example.com" and
|
||||||
|
is_jwt_valid(http.request.jwt.payload["{config_id}"][0]))
|
||||||
|
|
||||||
|
// Rate: 100 req/60s
|
||||||
|
// Counting expression: lookup_json_string(http.request.jwt.payload["{config_id}"][0], "sub")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volumetric Abuse Response
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Detect abnormal traffic spikes
|
||||||
|
(cf.api_gateway.volumetric_abuse_detected and http.host eq "api.example.com")
|
||||||
|
// Action: Challenge or Rate Limit
|
||||||
|
|
||||||
|
// Combined with rate limiting
|
||||||
|
(cf.api_gateway.volumetric_abuse_detected or
|
||||||
|
cf.threat_score gt 50) and http.host eq "api.example.com"
|
||||||
|
// Action: JS Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
## GraphQL Protection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Block oversized queries
|
||||||
|
(http.request.uri.path eq "/graphql" and
|
||||||
|
cf.api_gateway.graphql_query_size gt 100000)
|
||||||
|
// Action: Block
|
||||||
|
|
||||||
|
// Block deep nested queries
|
||||||
|
(http.request.uri.path eq "/graphql" and
|
||||||
|
cf.api_gateway.graphql_query_depth gt 10)
|
||||||
|
// Action: Block
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
**Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management
|
||||||
|
**Partner API:** mTLS + Schema Validation + Sequence Mitigation
|
||||||
|
**Internal API:** Discovery + Schema Learning + Auth Posture
|
||||||
|
|
||||||
|
## OWASP API Security Top 10 Mapping (2026)
|
||||||
|
|
||||||
|
| OWASP Issue | API Shield Solutions |
|
||||||
|
|-------------|---------------------|
|
||||||
|
| API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting |
|
||||||
|
| API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management |
|
||||||
|
| API3:2023 Broken Object Property Auth | Schema validation, JWT validation |
|
||||||
|
| API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management |
|
||||||
|
| API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture |
|
||||||
|
| API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management |
|
||||||
|
| API7:2023 SSRF | Schema validation, WAF managed rules |
|
||||||
|
| API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules |
|
||||||
|
| API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture |
|
||||||
|
| API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed |
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
**Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield
|
||||||
|
**Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields
|
||||||
|
**Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult
|
||||||
|
|
||||||
|
## Availability (2026)
|
||||||
|
|
||||||
|
| Feature | Availability | Notes |
|
||||||
|
|---------|-------------|-------|
|
||||||
|
| mTLS (CF-managed CA) | All plans | Self-service |
|
||||||
|
| Endpoint Management | All plans | Limited operations |
|
||||||
|
| Schema Validation 2.0 | All plans | Limited operations |
|
||||||
|
| API Discovery | Enterprise | 10K+ ops |
|
||||||
|
| JWT Validation | Enterprise add-on | Full validation |
|
||||||
|
| BOLA Detection | Enterprise add-on | Requires session IDs |
|
||||||
|
| Auth Posture | Enterprise add-on | Security audit |
|
||||||
|
| Volumetric Abuse Detection | Enterprise add-on | Traffic analysis |
|
||||||
|
| GraphQL Protection | Enterprise add-on | Query limits |
|
||||||
|
| Sequence Mitigation | Enterprise (beta) | Contact team |
|
||||||
|
| Full Suite | Enterprise add-on | All features |
|
||||||
|
|
||||||
|
**Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [configuration.md](configuration.md) - Setup all features before creating rules
|
||||||
|
- [api.md](api.md) - Firewall field reference and API endpoints
|
||||||
|
- [gotchas.md](gotchas.md) - Common issues and limits
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Cloudflare API Integration
|
||||||
|
|
||||||
|
Guide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting.
|
||||||
|
|
||||||
|
## Quick Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
How are you calling the Cloudflare API?
|
||||||
|
├─ From Workers runtime → Use bindings, not REST API (see ../bindings/)
|
||||||
|
├─ Server-side (Node/Python/Go) → Official SDK (see api.md)
|
||||||
|
├─ CLI/scripts → Wrangler or curl (see configuration.md)
|
||||||
|
├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/
|
||||||
|
└─ One-off requests → curl examples (see api.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
## SDK Selection
|
||||||
|
|
||||||
|
| Language | Package | Best For | Default Retries |
|
||||||
|
|----------|---------|----------|-----------------|
|
||||||
|
| TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 |
|
||||||
|
| Python | `cloudflare` | FastAPI, Django, scripts | 2 |
|
||||||
|
| Go | `cloudflare-go/v4` | CLI tools, microservices | 10 |
|
||||||
|
|
||||||
|
All SDKs are Stainless-generated from OpenAPI spec (consistent APIs).
|
||||||
|
|
||||||
|
## Authentication Methods
|
||||||
|
|
||||||
|
| Method | Security | Use Case | Scope |
|
||||||
|
|--------|----------|----------|-------|
|
||||||
|
| **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account |
|
||||||
|
| API Key + Email | Full account access | Legacy only | Everything |
|
||||||
|
| User Service Key | Limited | Origin CA certs only | Origin CA |
|
||||||
|
|
||||||
|
**Always use API tokens** for new projects.
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
| Limit | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Per user/token | 1200 requests / 5 minutes |
|
||||||
|
| Per IP | 200 requests / second |
|
||||||
|
| GraphQL | 320 / 5 minutes (cost-based) |
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Files to Read |
|
||||||
|
|------|---------------|
|
||||||
|
| Initialize SDK client | api.md |
|
||||||
|
| Configure auth/timeout/retry | configuration.md |
|
||||||
|
| Find usage patterns | patterns.md |
|
||||||
|
| Debug errors/rate limits | gotchas.md |
|
||||||
|
| Product-specific APIs | [Workers docs](https://developers.cloudflare.com/workers/), ../r2/, ../kv/, etc. |
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples
|
||||||
|
- **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup
|
||||||
|
- **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows
|
||||||
|
- **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Cloudflare API Docs](https://developers.cloudflare.com/api/)
|
||||||
|
- [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API)
|
||||||
|
- [Wrangler Reference](https://developers.cloudflare.com/workers/wrangler/) - CLI tool for Cloudflare development
|
||||||
|
- [GraphQL Analytics API Reference](../graphql-api/) - Analytics data via GraphQL (separate endpoint from REST API)
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
## Client Initialization
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from 'cloudflare';
|
||||||
|
|
||||||
|
const client = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cloudflare import Cloudflare
|
||||||
|
|
||||||
|
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
|
||||||
|
|
||||||
|
# For async:
|
||||||
|
from cloudflare import AsyncCloudflare
|
||||||
|
client = AsyncCloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/cloudflare/cloudflare-go/v4"
|
||||||
|
"github.com/cloudflare/cloudflare-go/v4/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := cloudflare.NewClient(
|
||||||
|
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### API Token (Recommended)
|
||||||
|
|
||||||
|
**Create token**: Dashboard → My Profile → API Tokens → Create Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLOUDFLARE_API_TOKEN='your-token-here'
|
||||||
|
|
||||||
|
curl "https://api.cloudflare.com/client/v4/zones" \
|
||||||
|
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token scopes**: Always use minimal permissions (zone-specific, time-limited).
|
||||||
|
|
||||||
|
### API Key (Legacy)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.cloudflare.com/client/v4/zones" \
|
||||||
|
--header "X-Auth-Email: user@example.com" \
|
||||||
|
--header "X-Auth-Key: $CLOUDFLARE_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Not recommended:** Full account access, cannot scope permissions.
|
||||||
|
|
||||||
|
## Auto-Pagination
|
||||||
|
|
||||||
|
All SDKs support automatic pagination for list operations.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript: for await...of
|
||||||
|
for await (const zone of client.zones.list()) {
|
||||||
|
console.log(zone.id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python: iterator protocol
|
||||||
|
for zone in client.zones.list():
|
||||||
|
print(zone.id)
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Go: ListAutoPaging
|
||||||
|
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
|
||||||
|
for iter.Next() {
|
||||||
|
zone := iter.Current()
|
||||||
|
fmt.Println(zone.ID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const zone = await client.zones.get({ zone_id: 'xxx' });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Cloudflare.NotFoundError) {
|
||||||
|
// 404
|
||||||
|
} else if (err instanceof Cloudflare.RateLimitError) {
|
||||||
|
// 429 - SDK auto-retries with backoff
|
||||||
|
} else if (err instanceof Cloudflare.APIError) {
|
||||||
|
console.log(err.status, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Error Types:**
|
||||||
|
- `AuthenticationError` (401) - Invalid token
|
||||||
|
- `PermissionDeniedError` (403) - Insufficient scope
|
||||||
|
- `NotFoundError` (404) - Resource not found
|
||||||
|
- `RateLimitError` (429) - Rate limit exceeded
|
||||||
|
- `InternalServerError` (≥500) - Cloudflare error
|
||||||
|
|
||||||
|
## Zone Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List zones
|
||||||
|
const zones = await client.zones.list({
|
||||||
|
account: { id: 'account-id' },
|
||||||
|
status: 'active',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create zone
|
||||||
|
const zone = await client.zones.create({
|
||||||
|
account: { id: 'account-id' },
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'full', // or 'partial'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update zone
|
||||||
|
await client.zones.edit('zone-id', {
|
||||||
|
paused: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete zone
|
||||||
|
await client.zones.delete('zone-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Go: requires cloudflare.F() wrapper
|
||||||
|
zone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||||
|
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
|
||||||
|
ID: cloudflare.F("account-id"),
|
||||||
|
}),
|
||||||
|
Name: cloudflare.F("example.com"),
|
||||||
|
Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## DNS Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create DNS record
|
||||||
|
await client.dns.records.create({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
name: 'subdomain.example.com',
|
||||||
|
content: '192.0.2.1',
|
||||||
|
ttl: 1, // auto
|
||||||
|
proxied: true, // Orange cloud
|
||||||
|
});
|
||||||
|
|
||||||
|
// List DNS records (with auto-pagination)
|
||||||
|
for await (const record of client.dns.records.list({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
})) {
|
||||||
|
console.log(record.name, record.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update DNS record
|
||||||
|
await client.dns.records.update({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
dns_record_id: 'record-id',
|
||||||
|
type: 'A',
|
||||||
|
name: 'subdomain.example.com',
|
||||||
|
content: '203.0.113.1',
|
||||||
|
proxied: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete DNS record
|
||||||
|
await client.dns.records.delete({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
dns_record_id: 'record-id',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python example
|
||||||
|
client.dns.records.create(
|
||||||
|
zone_id="zone-id",
|
||||||
|
type="A",
|
||||||
|
name="subdomain.example.com",
|
||||||
|
content="192.0.2.1",
|
||||||
|
ttl=1,
|
||||||
|
proxied=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [configuration.md](./configuration.md) - SDK configuration, environment variables
|
||||||
|
- [patterns.md](./patterns.md) - Real-world patterns and workflows
|
||||||
|
- [gotchas.md](./gotchas.md) - Rate limits, troubleshooting
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Set Variables
|
||||||
|
|
||||||
|
| Platform | Command |
|
||||||
|
|----------|---------|
|
||||||
|
| Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` |
|
||||||
|
| PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` |
|
||||||
|
| Windows CMD | `set CLOUDFLARE_API_TOKEN=token` |
|
||||||
|
|
||||||
|
**Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers.
|
||||||
|
|
||||||
|
### .env File Pattern
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env (add to .gitignore)
|
||||||
|
CLOUDFLARE_API_TOKEN=your-token-here
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
const client = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
client = Cloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## SDK Configuration
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
timeout: 120000, // 2 min (default 60s), in milliseconds
|
||||||
|
maxRetries: 5, // default 2
|
||||||
|
baseURL: 'https://...', // proxy (rare)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per-request overrides
|
||||||
|
await client.zones.get(
|
||||||
|
{ zone_id: 'zone-id' },
|
||||||
|
{ timeout: 5000, maxRetries: 0 }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = Cloudflare(
|
||||||
|
api_token=os.environ["CLOUDFLARE_API_TOKEN"],
|
||||||
|
timeout=120, # seconds (default 60)
|
||||||
|
max_retries=5, # default 2
|
||||||
|
base_url="https://...", # proxy (rare)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Per-request overrides
|
||||||
|
client.with_options(timeout=5, max_retries=0).zones.get(zone_id="zone-id")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```go
|
||||||
|
client := cloudflare.NewClient(
|
||||||
|
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
|
||||||
|
option.WithMaxRetries(5), // default 10 (higher than TS/Python)
|
||||||
|
option.WithRequestTimeout(2 * time.Minute), // default 60s
|
||||||
|
option.WithBaseURL("https://..."), // proxy (rare)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Per-request overrides
|
||||||
|
client.Zones.Get(ctx, "zone-id", option.WithMaxRetries(0))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | TypeScript | Python | Go | Default |
|
||||||
|
|--------|-----------|--------|-----|---------|
|
||||||
|
| Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s |
|
||||||
|
| Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) |
|
||||||
|
| Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com |
|
||||||
|
|
||||||
|
**Note:** Go SDK has higher default retries (10) than TypeScript/Python (2).
|
||||||
|
|
||||||
|
## Timeout Configuration
|
||||||
|
|
||||||
|
**When to increase:**
|
||||||
|
- Large zone transfers
|
||||||
|
- Bulk DNS operations
|
||||||
|
- Worker script uploads
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = new Cloudflare({
|
||||||
|
timeout: 300000, // 5 minutes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retry Configuration
|
||||||
|
|
||||||
|
**When to increase:** Rate-limit-heavy workflows, flaky network
|
||||||
|
|
||||||
|
**When to decrease:** Fast-fail requirements, user-facing requests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Increase retries for batch operations
|
||||||
|
const client = new Cloudflare({ maxRetries: 10 });
|
||||||
|
|
||||||
|
// Disable retries for fast-fail
|
||||||
|
const fastClient = new Cloudflare({ maxRetries: 0 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrangler CLI Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure authentication
|
||||||
|
wrangler login
|
||||||
|
# Or
|
||||||
|
export CLOUDFLARE_API_TOKEN='token'
|
||||||
|
|
||||||
|
# Common commands that use API
|
||||||
|
wrangler deploy # Uploads worker via API
|
||||||
|
wrangler kv:key put # KV operations
|
||||||
|
wrangler r2 bucket create # R2 operations
|
||||||
|
wrangler d1 execute # D1 operations
|
||||||
|
wrangler pages deploy # Pages operations
|
||||||
|
|
||||||
|
# Get API configuration
|
||||||
|
wrangler whoami # Shows authenticated user
|
||||||
|
```
|
||||||
|
|
||||||
|
### wrangler.toml
|
||||||
|
|
||||||
|
```toml
|
||||||
|
name = "my-worker"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2024-01-01"
|
||||||
|
account_id = "your-account-id"
|
||||||
|
|
||||||
|
# Can also use env vars:
|
||||||
|
# CLOUDFLARE_ACCOUNT_ID
|
||||||
|
# CLOUDFLARE_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [api.md](./api.md) - Client initialization, authentication
|
||||||
|
- [gotchas.md](./gotchas.md) - Rate limits, timeout errors
|
||||||
|
- [Wrangler Reference](https://developers.cloudflare.com/workers/wrangler/) - CLI tool details
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Gotchas & Troubleshooting
|
||||||
|
|
||||||
|
## Rate Limits & 429 Errors
|
||||||
|
|
||||||
|
**Actual Limits:**
|
||||||
|
- **1200 requests / 5 minutes** per user/token (global)
|
||||||
|
- **200 requests / second** per IP address
|
||||||
|
- **GraphQL: 320 / 5 minutes** (cost-based)
|
||||||
|
|
||||||
|
**SDK Behavior:**
|
||||||
|
- Auto-retry with exponential backoff (default 2 retries, Go: 10)
|
||||||
|
- Respects `Retry-After` header
|
||||||
|
- Throws `RateLimitError` after exhausting retries
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Increase retries for rate-limit-heavy workflows
|
||||||
|
const client = new Cloudflare({ maxRetries: 5 });
|
||||||
|
|
||||||
|
// Add application-level throttling
|
||||||
|
import pLimit from 'p-limit';
|
||||||
|
const limit = pLimit(10); // Max 10 concurrent requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## SDK-Specific Issues
|
||||||
|
|
||||||
|
### Go: Required Field Wrapper
|
||||||
|
|
||||||
|
**Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ❌ WRONG - Won't compile or send field
|
||||||
|
client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||||
|
Name: "example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ CORRECT
|
||||||
|
client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||||
|
Name: cloudflare.F("example.com"),
|
||||||
|
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
|
||||||
|
ID: cloudflare.F("account-id"),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Distinguishes between zero value, null, and omitted fields.
|
||||||
|
|
||||||
|
### Python: Async vs Sync Clients
|
||||||
|
|
||||||
|
**Problem:** Using sync client in async context or vice versa.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ WRONG - Can't await sync client
|
||||||
|
from cloudflare import Cloudflare
|
||||||
|
client = Cloudflare()
|
||||||
|
await client.zones.list() # TypeError
|
||||||
|
|
||||||
|
# ✅ CORRECT - Use AsyncCloudflare
|
||||||
|
from cloudflare import AsyncCloudflare
|
||||||
|
client = AsyncCloudflare()
|
||||||
|
await client.zones.list()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Token Permission Errors (403)
|
||||||
|
|
||||||
|
**Problem:** API returns 403 Forbidden despite valid token.
|
||||||
|
|
||||||
|
**Cause:** Token lacks required permissions (scope).
|
||||||
|
|
||||||
|
**Scopes Required:**
|
||||||
|
|
||||||
|
| Operation | Required Scope |
|
||||||
|
|-----------|----------------|
|
||||||
|
| List zones | Zone:Read (zone-level or account-level) |
|
||||||
|
| Create zone | Zone:Edit (account-level) |
|
||||||
|
| Edit DNS | DNS:Edit (zone-level) |
|
||||||
|
| Deploy Worker | Workers Script:Edit (account-level) |
|
||||||
|
| Read KV | Workers KV Storage:Read |
|
||||||
|
| Write KV | Workers KV Storage:Edit |
|
||||||
|
|
||||||
|
**Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens.
|
||||||
|
|
||||||
|
## Pagination Truncation
|
||||||
|
|
||||||
|
**Problem:** Only getting first 20 results (default page size).
|
||||||
|
|
||||||
|
**Solution:** Use auto-pagination iterators.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG - Only first page (20 items)
|
||||||
|
const page = await client.zones.list();
|
||||||
|
|
||||||
|
// ✅ CORRECT - All results
|
||||||
|
const zones = [];
|
||||||
|
for await (const zone of client.zones.list()) {
|
||||||
|
zones.push(zone);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workers Subrequests
|
||||||
|
|
||||||
|
**Problem:** Rate limit hit faster than expected in Workers.
|
||||||
|
|
||||||
|
**Cause:** Workers subrequests count as separate API calls.
|
||||||
|
|
||||||
|
**Solution:** Use bindings instead of REST API in Workers (see ../bindings/).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG - REST API in Workers (counts against rate limit)
|
||||||
|
const client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN });
|
||||||
|
const zones = await client.zones.list();
|
||||||
|
|
||||||
|
// ✅ CORRECT - Use bindings (no rate limit)
|
||||||
|
// Access via env.MY_BINDING
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Errors (401)
|
||||||
|
|
||||||
|
**Problem:** "Authentication failed" or "Invalid token"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Token expired
|
||||||
|
- Token deleted/revoked
|
||||||
|
- Token not set in environment
|
||||||
|
- Wrong token format
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Verify token is set
|
||||||
|
if (!process.env.CLOUDFLARE_API_TOKEN) {
|
||||||
|
throw new Error('CLOUDFLARE_API_TOKEN not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test token
|
||||||
|
const user = await client.user.tokens.verify();
|
||||||
|
console.log('Token valid:', user.status);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeout Errors
|
||||||
|
|
||||||
|
**Problem:** Request times out (default 60s).
|
||||||
|
|
||||||
|
**Cause:** Large operations (bulk DNS, zone transfers).
|
||||||
|
|
||||||
|
**Solution:** Increase timeout or split operations.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Increase timeout
|
||||||
|
const client = new Cloudflare({
|
||||||
|
timeout: 300000, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or split operations
|
||||||
|
const batchSize = 100;
|
||||||
|
for (let i = 0; i < records.length; i += batchSize) {
|
||||||
|
const batch = records.slice(i, i + batchSize);
|
||||||
|
await processBatch(batch);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zone Not Found (404)
|
||||||
|
|
||||||
|
**Problem:** Zone ID valid but returns 404.
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
- Zone not in account associated with token
|
||||||
|
- Zone deleted
|
||||||
|
- Wrong zone ID format
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List all zones to find correct ID
|
||||||
|
for await (const zone of client.zones.list()) {
|
||||||
|
console.log(zone.id, zone.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits Reference
|
||||||
|
|
||||||
|
| Resource/Limit | Value | Notes |
|
||||||
|
|----------------|-------|-------|
|
||||||
|
| API rate limit | 1200/5min | Per user/token |
|
||||||
|
| IP rate limit | 200/sec | Per IP |
|
||||||
|
| GraphQL rate limit | 320/5min | Cost-based |
|
||||||
|
| Parallel requests (recommended) | < 10 | Avoid overwhelming API |
|
||||||
|
| Default page size | 20 | Use auto-pagination |
|
||||||
|
| Max page size | 50 | Some endpoints |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Never commit tokens
|
||||||
|
- Use minimal permissions
|
||||||
|
- Rotate tokens regularly
|
||||||
|
- Set token expiration
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- Batch operations
|
||||||
|
- Use pagination wisely
|
||||||
|
- Cache responses
|
||||||
|
- Handle rate limits
|
||||||
|
|
||||||
|
**Code Organization:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create reusable client instance
|
||||||
|
export const cfClient = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
maxRetries: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wrap common operations
|
||||||
|
export async function getZoneDetails(zoneId: string) {
|
||||||
|
return await cfClient.zones.get({ zone_id: zoneId });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [api.md](./api.md) - Error types, authentication
|
||||||
|
- [configuration.md](./configuration.md) - Timeout/retry configuration
|
||||||
|
- [patterns.md](./patterns.md) - Error handling patterns
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Common Patterns
|
||||||
|
|
||||||
|
## List All with Auto-Pagination
|
||||||
|
|
||||||
|
**Problem:** API returns paginated results. Default page size is 20.
|
||||||
|
|
||||||
|
**Solution:** Use SDK auto-pagination to iterate all results.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript
|
||||||
|
for await (const zone of client.zones.list()) {
|
||||||
|
console.log(zone.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python
|
||||||
|
for zone in client.zones.list():
|
||||||
|
print(zone.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Go
|
||||||
|
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
|
||||||
|
for iter.Next() {
|
||||||
|
fmt.Println(iter.Current().Name)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling with Retry
|
||||||
|
|
||||||
|
**Problem:** Rate limits (429) and transient errors need retry.
|
||||||
|
|
||||||
|
**Solution:** SDKs auto-retry with exponential backoff. Customize as needed.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Increase retries for rate-limit-heavy operations
|
||||||
|
const client = new Cloudflare({ maxRetries: 5 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const zone = await client.zones.create({ /* ... */ });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Cloudflare.RateLimitError) {
|
||||||
|
// Already retried 5 times with backoff
|
||||||
|
const retryAfter = err.headers['retry-after'];
|
||||||
|
console.log(`Rate limited. Retry after ${retryAfter}s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Parallel Operations
|
||||||
|
|
||||||
|
**Problem:** Need to create multiple resources quickly.
|
||||||
|
|
||||||
|
**Solution:** Use `Promise.all()` for parallel requests (respect rate limits).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create multiple DNS records in parallel
|
||||||
|
const records = ['www', 'api', 'cdn'].map(subdomain =>
|
||||||
|
client.dns.records.create({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
name: `${subdomain}.example.com`,
|
||||||
|
content: '192.0.2.1',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.all(records);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controlled concurrency** (avoid rate limits):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import pLimit from 'p-limit';
|
||||||
|
const limit = pLimit(10); // Max 10 concurrent
|
||||||
|
|
||||||
|
const subdomains = ['www', 'api', 'cdn', /* many more */];
|
||||||
|
const records = subdomains.map(subdomain =>
|
||||||
|
limit(() => client.dns.records.create({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
name: `${subdomain}.example.com`,
|
||||||
|
content: '192.0.2.1',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
await Promise.all(records);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zone CRUD Workflow
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create
|
||||||
|
const zone = await client.zones.create({
|
||||||
|
account: { id: 'account-id' },
|
||||||
|
name: 'example.com',
|
||||||
|
type: 'full',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read
|
||||||
|
const fetched = await client.zones.get({ zone_id: zone.id });
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await client.zones.edit(zone.id, { paused: false });
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await client.zones.delete(zone.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## DNS Bulk Update
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fetch all A records
|
||||||
|
const records = [];
|
||||||
|
for await (const record of client.dns.records.list({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
})) {
|
||||||
|
records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all to new IP
|
||||||
|
await Promise.all(records.map(record =>
|
||||||
|
client.dns.records.update({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
dns_record_id: record.id,
|
||||||
|
type: 'A',
|
||||||
|
name: record.name,
|
||||||
|
content: '203.0.113.1', // New IP
|
||||||
|
proxied: record.proxied,
|
||||||
|
ttl: record.ttl,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filter and Collect Results
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find all proxied A records
|
||||||
|
const proxiedRecords = [];
|
||||||
|
for await (const record of client.dns.records.list({
|
||||||
|
zone_id: 'zone-id',
|
||||||
|
type: 'A',
|
||||||
|
})) {
|
||||||
|
if (record.proxied) {
|
||||||
|
proxiedRecords.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Recovery Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function createZoneWithRetry(name: string, maxAttempts = 3) {
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await client.zones.create({
|
||||||
|
account: { id: 'account-id' },
|
||||||
|
name,
|
||||||
|
type: 'full',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) {
|
||||||
|
const retryAfter = parseInt(err.headers['retry-after'] || '5');
|
||||||
|
console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional Update Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Only update if zone is active
|
||||||
|
const zone = await client.zones.get({ zone_id: 'zone-id' });
|
||||||
|
if (zone.status === 'active') {
|
||||||
|
await client.zones.edit(zone.id, { paused: false });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch with Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Process multiple zones, continue on errors
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
zoneIds.map(id => client.zones.get({ zone_id: id }))
|
||||||
|
);
|
||||||
|
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
console.log(`Zone ${i}: ${result.value.name}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Zone ${i} failed:`, result.reason.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [api.md](./api.md) - SDK client initialization, basic operations
|
||||||
|
- [gotchas.md](./gotchas.md) - Rate limits, common errors
|
||||||
|
- [configuration.md](./configuration.md) - SDK configuration options
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Cloudflare Argo Smart Routing Skill Reference
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Cloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network.
|
||||||
|
|
||||||
|
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Enable via cURL
|
||||||
|
```bash
|
||||||
|
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||||
|
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"value": "on"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable via TypeScript SDK
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from 'cloudflare';
|
||||||
|
|
||||||
|
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
|
||||||
|
|
||||||
|
const result = await client.argo.smartRouting.edit({
|
||||||
|
zone_id: 'your-zone-id',
|
||||||
|
value: 'on',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Argo enabled: ${result.value}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
- **Intelligent routing**: Detects congestion, outages, packet loss in real-time
|
||||||
|
- **Global optimization**: Routes across 300+ Cloudflare data centers
|
||||||
|
- **Automatic failover**: Switches paths when issues detected (typically <1s)
|
||||||
|
- **Works with existing setup**: No origin changes required
|
||||||
|
|
||||||
|
### Billing Model
|
||||||
|
- Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic)
|
||||||
|
- Requires billing configuration before enabling
|
||||||
|
- Available on Enterprise+ plans (check zone eligibility)
|
||||||
|
|
||||||
|
### When to Use
|
||||||
|
- **High-traffic production sites** with global user base
|
||||||
|
- **Latency-sensitive applications** (APIs, real-time services)
|
||||||
|
- **Sites behind Cloudflare proxy** (orange-clouded DNS records)
|
||||||
|
- **Combined with Tiered Cache** for maximum performance gains
|
||||||
|
|
||||||
|
### When NOT to Use
|
||||||
|
- Development/staging environments (cost control)
|
||||||
|
- Low-traffic sites (<1TB/month) where cost may exceed benefit
|
||||||
|
- Sites with primarily single-region traffic
|
||||||
|
|
||||||
|
## Should I Enable Argo?
|
||||||
|
|
||||||
|
| Your Situation | Recommendation |
|
||||||
|
|----------------|----------------|
|
||||||
|
| Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive |
|
||||||
|
| Enterprise plan, latency-critical APIs | ✅ Enable - performance matters |
|
||||||
|
| Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify |
|
||||||
|
| Development/staging environment | ❌ Disable - use in production only |
|
||||||
|
| Not yet configured billing | ❌ Configure billing first |
|
||||||
|
|
||||||
|
## Reading Order by Task
|
||||||
|
|
||||||
|
| Your Goal | Start With | Then Read |
|
||||||
|
|-----------|------------|-----------|
|
||||||
|
| Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) |
|
||||||
|
| Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) |
|
||||||
|
| Terraform/IaC setup | [configuration.md](configuration.md) | - |
|
||||||
|
| Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) |
|
||||||
|
| Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) |
|
||||||
|
| Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) |
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples
|
||||||
|
- **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration
|
||||||
|
- **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns
|
||||||
|
- **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
|
||||||
|
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
|
||||||
|
- [Spectrum Documentation](https://developers.cloudflare.com/spectrum/)
|
||||||
|
- [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/)
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
## API Reference
|
||||||
|
|
||||||
|
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes.
|
||||||
|
|
||||||
|
### Base Endpoint
|
||||||
|
```
|
||||||
|
https://api.cloudflare.com/client/v4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
Use API tokens with Zone:Argo Smart Routing:Edit permissions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Headers required
|
||||||
|
X-Auth-Email: user@example.com
|
||||||
|
Authorization: Bearer YOUR_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Argo Smart Routing Status
|
||||||
|
|
||||||
|
**Endpoint:** `GET /zones/{zone_id}/argo/smart_routing`
|
||||||
|
|
||||||
|
**Description:** Retrieves current Argo Smart Routing enablement status.
|
||||||
|
|
||||||
|
**cURL Example:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||||
|
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"id": "smart_routing",
|
||||||
|
"value": "on",
|
||||||
|
"editable": true,
|
||||||
|
"modified_on": "2024-01-11T12:00:00Z"
|
||||||
|
},
|
||||||
|
"success": true,
|
||||||
|
"errors": [],
|
||||||
|
"messages": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript SDK Example:**
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from 'cloudflare';
|
||||||
|
|
||||||
|
const client = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' });
|
||||||
|
console.log(`Argo status: ${status.value}, editable: ${status.editable}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python SDK Example:**
|
||||||
|
```python
|
||||||
|
from cloudflare import Cloudflare
|
||||||
|
|
||||||
|
client = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN'))
|
||||||
|
|
||||||
|
status = client.argo.smart_routing.get(zone_id='your-zone-id')
|
||||||
|
print(f"Argo status: {status.value}, editable: {status.editable}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Argo Smart Routing Status
|
||||||
|
|
||||||
|
**Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing`
|
||||||
|
|
||||||
|
**Description:** Enable or disable Argo Smart Routing for a zone.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": "on" // or "off"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**cURL Example:**
|
||||||
|
```bash
|
||||||
|
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||||
|
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"value": "on"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript SDK Example:**
|
||||||
|
```typescript
|
||||||
|
const result = await client.argo.smartRouting.edit({
|
||||||
|
zone_id: 'your-zone-id',
|
||||||
|
value: 'on',
|
||||||
|
});
|
||||||
|
console.log(`Updated: ${result.value} at ${result.modified_on}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python SDK Example:**
|
||||||
|
```python
|
||||||
|
result = client.argo.smart_routing.edit(
|
||||||
|
zone_id='your-zone-id',
|
||||||
|
value='on'
|
||||||
|
)
|
||||||
|
print(f"Updated: {result.value} at {result.modified_on}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking Editability Before Updates
|
||||||
|
|
||||||
|
**Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations).
|
||||||
|
|
||||||
|
**Pattern:**
|
||||||
|
```typescript
|
||||||
|
async function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise<boolean> {
|
||||||
|
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||||
|
|
||||||
|
if (!status.editable) {
|
||||||
|
console.error('Cannot modify Argo: editable=false (check billing/permissions)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.value === 'on') {
|
||||||
|
console.log('Argo already enabled');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||||
|
console.log('Argo enabled successfully');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python Pattern:**
|
||||||
|
```python
|
||||||
|
def safely_enable_argo(client: Cloudflare, zone_id: str) -> bool:
|
||||||
|
status = client.argo.smart_routing.get(zone_id=zone_id)
|
||||||
|
|
||||||
|
if not status.editable:
|
||||||
|
print('Cannot modify Argo: editable=false (check billing/permissions)')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if status.value == 'on':
|
||||||
|
print('Argo already enabled')
|
||||||
|
return True
|
||||||
|
|
||||||
|
client.argo.smart_routing.edit(zone_id=zone_id, value='on')
|
||||||
|
print('Argo enabled successfully')
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The TypeScript SDK provides typed error classes for robust error handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from 'cloudflare';
|
||||||
|
import { APIError, APIConnectionError, RateLimitError } from 'cloudflare';
|
||||||
|
|
||||||
|
async function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) {
|
||||||
|
try {
|
||||||
|
const result = await client.argo.smartRouting.edit({
|
||||||
|
zone_id: zoneId,
|
||||||
|
value: 'on',
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RateLimitError) {
|
||||||
|
console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after'));
|
||||||
|
// Implement exponential backoff
|
||||||
|
} else if (error instanceof APIError) {
|
||||||
|
console.error('API error:', error.status, error.message);
|
||||||
|
if (error.status === 403) {
|
||||||
|
console.error('Permission denied - check API token scopes');
|
||||||
|
} else if (error.status === 400) {
|
||||||
|
console.error('Bad request - verify zone_id and payload');
|
||||||
|
}
|
||||||
|
} else if (error instanceof APIConnectionError) {
|
||||||
|
console.error('Connection failed:', error.message);
|
||||||
|
// Retry with exponential backoff
|
||||||
|
} else {
|
||||||
|
console.error('Unexpected error:', error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python Error Handling:**
|
||||||
|
```python
|
||||||
|
from cloudflare import Cloudflare, APIError, RateLimitError
|
||||||
|
|
||||||
|
def enable_argo_with_error_handling(client: Cloudflare, zone_id: str):
|
||||||
|
try:
|
||||||
|
result = client.argo.smart_routing.edit(zone_id=zone_id, value='on')
|
||||||
|
return result
|
||||||
|
except RateLimitError as e:
|
||||||
|
print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}")
|
||||||
|
raise
|
||||||
|
except APIError as e:
|
||||||
|
print(f"API error: {e.status} - {e.message}")
|
||||||
|
if e.status == 403:
|
||||||
|
print('Permission denied - check API token scopes')
|
||||||
|
elif e.status == 400:
|
||||||
|
print('Bad request - verify zone_id and payload')
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Schema
|
||||||
|
|
||||||
|
All Argo Smart Routing API responses follow this structure:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ArgoSmartRoutingResponse {
|
||||||
|
result: {
|
||||||
|
id: 'smart_routing';
|
||||||
|
value: 'on' | 'off';
|
||||||
|
editable: boolean;
|
||||||
|
modified_on: string; // ISO 8601 timestamp
|
||||||
|
};
|
||||||
|
success: boolean;
|
||||||
|
errors: Array<{
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
messages: Array<string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Response Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `value` | `"on" \| "off"` | Current enablement status |
|
||||||
|
| `editable` | `boolean` | Whether changes are allowed (check before PATCH) |
|
||||||
|
| `modified_on` | `string` | ISO timestamp of last modification |
|
||||||
|
| `success` | `boolean` | Whether request succeeded |
|
||||||
|
| `errors` | `Array` | Error details if `success: false`
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
## Configuration Management
|
||||||
|
|
||||||
|
**Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged.
|
||||||
|
|
||||||
|
### Infrastructure as Code (Terraform)
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# terraform/argo.tf
|
||||||
|
# Note: Use Cloudflare Terraform provider
|
||||||
|
|
||||||
|
resource "cloudflare_argo" "example" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
smart_routing = "on"
|
||||||
|
tiered_caching = "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "zone_id" {
|
||||||
|
description = "Cloudflare Zone ID"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
output "argo_enabled" {
|
||||||
|
value = cloudflare_argo.example.smart_routing
|
||||||
|
description = "Argo Smart Routing status"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment-Based Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// config/argo.ts
|
||||||
|
interface ArgoEnvironmentConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
tieredCache: boolean;
|
||||||
|
monitoring: {
|
||||||
|
usageAlerts: boolean;
|
||||||
|
threshold: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const configs: Record<string, ArgoEnvironmentConfig> = {
|
||||||
|
production: {
|
||||||
|
enabled: true,
|
||||||
|
tieredCache: true,
|
||||||
|
monitoring: {
|
||||||
|
usageAlerts: true,
|
||||||
|
threshold: 1000, // GB
|
||||||
|
},
|
||||||
|
},
|
||||||
|
staging: {
|
||||||
|
enabled: true,
|
||||||
|
tieredCache: false,
|
||||||
|
monitoring: {
|
||||||
|
usageAlerts: false,
|
||||||
|
threshold: 100, // GB
|
||||||
|
},
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
enabled: false,
|
||||||
|
tieredCache: false,
|
||||||
|
monitoring: {
|
||||||
|
usageAlerts: false,
|
||||||
|
threshold: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getArgoConfig(env: string): ArgoEnvironmentConfig {
|
||||||
|
return configs[env] || configs.development;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pulumi Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pulumi/argo.ts
|
||||||
|
import * as cloudflare from '@pulumi/cloudflare';
|
||||||
|
|
||||||
|
const zone = new cloudflare.Zone('example-zone', {
|
||||||
|
zone: 'example.com',
|
||||||
|
plan: 'enterprise',
|
||||||
|
});
|
||||||
|
|
||||||
|
const argoSettings = new cloudflare.Argo('argo-config', {
|
||||||
|
zoneId: zone.id,
|
||||||
|
smartRouting: 'on',
|
||||||
|
tieredCaching: 'on',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const argoEnabled = argoSettings.smartRouting;
|
||||||
|
export const zoneId = zone.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Billing Configuration
|
||||||
|
|
||||||
|
Before enabling Argo Smart Routing, ensure billing is configured for the account:
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
1. Valid payment method on file
|
||||||
|
2. Enterprise or higher plan
|
||||||
|
3. Zone must have billing enabled
|
||||||
|
|
||||||
|
**Check Billing Status via Dashboard:**
|
||||||
|
1. Navigate to Account → Billing
|
||||||
|
2. Verify payment method configured
|
||||||
|
3. Check zone subscription status
|
||||||
|
|
||||||
|
**Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses.
|
||||||
|
|
||||||
|
## Environment Variable Setup
|
||||||
|
|
||||||
|
**Required Environment Variables:**
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
CLOUDFLARE_API_TOKEN=your_api_token_here
|
||||||
|
CLOUDFLARE_ZONE_ID=your_zone_id_here
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
ARGO_ENABLED=true
|
||||||
|
ARGO_TIERED_CACHE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript Configuration Loader:**
|
||||||
|
```typescript
|
||||||
|
// config/env.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const envSchema = z.object({
|
||||||
|
CLOUDFLARE_API_TOKEN: z.string().min(1),
|
||||||
|
CLOUDFLARE_ZONE_ID: z.string().min(1),
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
|
||||||
|
ARGO_ENABLED: z.string().optional().default('false'),
|
||||||
|
ARGO_TIERED_CACHE: z.string().optional().default('false'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const env = envSchema.parse(process.env);
|
||||||
|
|
||||||
|
export const argoConfig = {
|
||||||
|
enabled: env.ARGO_ENABLED === 'true',
|
||||||
|
tieredCache: env.ARGO_TIERED_CACHE === 'true',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
**GitHub Actions Example:**
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy-argo.yml
|
||||||
|
name: Deploy Argo Configuration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'terraform/argo.tf'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Terraform
|
||||||
|
uses: hashicorp/setup-terraform@v2
|
||||||
|
|
||||||
|
- name: Terraform Init
|
||||||
|
run: terraform init
|
||||||
|
working-directory: ./terraform
|
||||||
|
|
||||||
|
- name: Terraform Apply
|
||||||
|
run: terraform apply -auto-approve
|
||||||
|
working-directory: ./terraform
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enterprise Preview Program
|
||||||
|
|
||||||
|
For early access to Argo Smart Routing features and Smart Shield integration:
|
||||||
|
|
||||||
|
**Eligibility:**
|
||||||
|
- Enterprise plan customers
|
||||||
|
- Active Cloudflare support contract
|
||||||
|
- Production traffic >100GB/month
|
||||||
|
|
||||||
|
**How to Join:**
|
||||||
|
1. Contact Cloudflare account team or support
|
||||||
|
2. Request Argo/Smart Shield preview access
|
||||||
|
3. Receive preview zone configuration
|
||||||
|
|
||||||
|
**Preview Features:**
|
||||||
|
- Enhanced analytics and reporting
|
||||||
|
- Smart Shield DDoS integration
|
||||||
|
- Advanced routing policies
|
||||||
|
- Priority support for routing issues
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
## Best Practices Summary
|
||||||
|
|
||||||
|
**Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates.
|
||||||
|
|
||||||
|
1. **Always check editability** before attempting to enable/disable Argo
|
||||||
|
2. **Set up billing notifications** to avoid unexpected costs
|
||||||
|
3. **Combine with Tiered Cache** for maximum performance benefit
|
||||||
|
4. **Use in production only** - disable for dev/staging to control costs
|
||||||
|
5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics
|
||||||
|
6. **Handle errors gracefully** - check for billing, permissions, zone compatibility
|
||||||
|
7. **Test configuration changes** in staging before production
|
||||||
|
8. **Use TypeScript SDK** for type safety and better developer experience
|
||||||
|
9. **Implement retry logic** for API calls in production systems
|
||||||
|
10. **Document zone-specific settings** for team visibility
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### "Argo unavailable"
|
||||||
|
|
||||||
|
**Problem:** API returns error "Argo Smart Routing is unavailable for this zone"
|
||||||
|
|
||||||
|
**Cause:** Zone not eligible or billing not set up
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Verify zone has Enterprise or higher plan
|
||||||
|
2. Check billing is configured in Account → Billing
|
||||||
|
3. Ensure payment method is valid and current
|
||||||
|
4. Contact Cloudflare support if eligibility unclear
|
||||||
|
|
||||||
|
### "Cannot enable/disable"
|
||||||
|
|
||||||
|
**Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response
|
||||||
|
|
||||||
|
**Cause:** Insufficient permissions or zone restrictions
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check API token has `Zone:Argo Smart Routing:Edit` permission
|
||||||
|
2. Verify `editable: true` in GET response before attempting PATCH
|
||||||
|
3. If `editable: false`, check:
|
||||||
|
- Billing configured for account
|
||||||
|
- Zone plan includes Argo (Enterprise+)
|
||||||
|
- No active zone holds or suspensions
|
||||||
|
- API token has correct scopes
|
||||||
|
|
||||||
|
### `editable: false` Error
|
||||||
|
|
||||||
|
**Problem:** GET request returns `"editable": false`, preventing enable/disable
|
||||||
|
|
||||||
|
**Cause:** Zone-level restrictions from billing, plan, or permissions
|
||||||
|
|
||||||
|
**Solution Pattern:**
|
||||||
|
```typescript
|
||||||
|
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||||
|
|
||||||
|
if (!status.editable) {
|
||||||
|
// Don't attempt to modify - will fail
|
||||||
|
console.error('Cannot modify Argo settings:');
|
||||||
|
console.error('- Check billing is configured');
|
||||||
|
console.error('- Verify zone has Enterprise+ plan');
|
||||||
|
console.error('- Confirm API token has Edit permission');
|
||||||
|
throw new Error('Argo is not editable for this zone');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe to proceed with enable/disable
|
||||||
|
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
**Problem:** `429 Too Many Requests` error from API
|
||||||
|
|
||||||
|
**Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```typescript
|
||||||
|
import { RateLimitError } from 'cloudflare';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RateLimitError) {
|
||||||
|
const retryAfter = error.response?.headers.get('retry-after');
|
||||||
|
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
|
||||||
|
|
||||||
|
// Implement exponential backoff
|
||||||
|
await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000));
|
||||||
|
// Retry request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Resource/Limit | Value | Notes |
|
||||||
|
|----------------|-------|-------|
|
||||||
|
| Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL |
|
||||||
|
| Zones supported | Enterprise+ | Check zone plan in dashboard |
|
||||||
|
| Billing requirement | Must be configured | Before enabling; verify payment method |
|
||||||
|
| API rate limit | 1200 req / 5 min | Per API token across all endpoints |
|
||||||
|
| Spectrum apps | No hard limit | Each app can enable Argo independently |
|
||||||
|
| Traffic counting | Proxied only | Only orange-clouded DNS records count |
|
||||||
|
| DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing |
|
||||||
|
| Analytics latency | 1-5 minutes | Real-time metrics not available |
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
|
||||||
|
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
|
||||||
|
- [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
|
||||||
|
- [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)
|
||||||
|
- [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Integration Patterns
|
||||||
|
|
||||||
|
## Enable Argo + Tiered Cache
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function enableOptimalPerformance(client: Cloudflare, zoneId: string) {
|
||||||
|
await Promise.all([
|
||||||
|
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
|
||||||
|
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin
|
||||||
|
|
||||||
|
**Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload
|
||||||
|
|
||||||
|
## Usage Analytics (GraphQL)
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query ArgoAnalytics($zoneTag: string!) {
|
||||||
|
viewer {
|
||||||
|
zones(filter: { zoneTag: $zoneTag }) {
|
||||||
|
httpRequestsAdaptiveGroups(limit: 1000) {
|
||||||
|
sum { argoBytes, bytes }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged.
|
||||||
|
|
||||||
|
## Spectrum TCP Integration
|
||||||
|
|
||||||
|
Enable Argo for non-HTTP traffic (databases, game servers, IoT):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Update existing app
|
||||||
|
await client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true });
|
||||||
|
|
||||||
|
// Create new app with Argo
|
||||||
|
await client.spectrum.apps.create({
|
||||||
|
zone_id: zoneId,
|
||||||
|
dns: { type: 'CNAME', name: 'tcp.example.com' },
|
||||||
|
origin_direct: ['tcp://origin.example.com:3306'],
|
||||||
|
protocol: 'tcp/3306',
|
||||||
|
argo_smart_routing: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22)
|
||||||
|
|
||||||
|
## Pre-Flight Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function validateArgoEligibility(client: Cloudflare, zoneId: string) {
|
||||||
|
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||||
|
const zone = await client.zones.get({ zone_id: zoneId });
|
||||||
|
|
||||||
|
const issues: string[] = [];
|
||||||
|
if (!status.editable) issues.push('Zone not editable');
|
||||||
|
if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan');
|
||||||
|
if (zone.status !== 'active') issues.push('Zone not active');
|
||||||
|
|
||||||
|
return { canEnable: issues.length === 0, issues };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Enable Verification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise<boolean> {
|
||||||
|
await new Promise(r => setTimeout(r, 2000)); // Wait for propagation
|
||||||
|
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||||
|
return status.value === 'on';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Setup Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function setupArgo(client: Cloudflare, zoneId: string) {
|
||||||
|
// 1. Validate
|
||||||
|
const { canEnable, issues } = await validateArgoEligibility(client, zoneId);
|
||||||
|
if (!canEnable) throw new Error(issues.join(', '));
|
||||||
|
|
||||||
|
// 2. Enable both features
|
||||||
|
await Promise.all([
|
||||||
|
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
|
||||||
|
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. Verify
|
||||||
|
const [argo, cache] = await Promise.all([
|
||||||
|
client.argo.smartRouting.get({ zone_id: zoneId }),
|
||||||
|
client.argo.tieredCaching.get({ zone_id: zoneId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { argo: argo.value === 'on', tieredCache: cache.value === 'on' };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Cloudflare Artifacts
|
||||||
|
|
||||||
|
Store versioned file trees behind a repo-style interface that works from Workers, the REST API, and Git-compatible tooling.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use **Artifacts** when the thing you need to store is a versioned filesystem tree rather than a single object, key, or SQL row.
|
||||||
|
|
||||||
|
Typical Artifacts use cases:
|
||||||
|
- Git-style repositories
|
||||||
|
- Per-agent, per-session, or per-task repos
|
||||||
|
- Build outputs and deployment bundles
|
||||||
|
- Checkpoints and generated assets
|
||||||
|
- Shared file trees passed between developer tools and Workers
|
||||||
|
|
||||||
|
Artifacts is a good fit when the same content needs to be addressable from **Workers**, the **REST API**, and **Git-compatible clients**.
|
||||||
|
|
||||||
|
Artifacts is especially useful for agent and automation workflows where each unit of work should have its own isolated repo and token.
|
||||||
|
|
||||||
|
**Prefer retrieval over memory** for current availability, authentication details, route shapes, limits, and pricing. Start at `https://developers.cloudflare.com/artifacts/`.
|
||||||
|
|
||||||
|
## When to Use Artifacts
|
||||||
|
|
||||||
|
| Need | Use | Why |
|
||||||
|
|------|-----|-----|
|
||||||
|
| Versioned file trees such as repos, build outputs, checkpoints, or generated assets | Artifacts | Artifacts stores and shares **versioned filesystem content** |
|
||||||
|
| A git-compatible workflow with `clone`, `fetch`, `pull`, or `push` | Artifacts | Artifacts exposes **git-over-HTTPS remotes** and repo-scoped tokens |
|
||||||
|
| The same artifact accessible from Workers, HTTP APIs, and developer tooling | Artifacts | Artifacts is available through a **Workers binding**, **REST API**, and **git-compatible interface** |
|
||||||
|
| Large files by object key, app config by key, or relational app data | R2, KV, or D1 | Use storage products directly when you need **objects, key-value entries, or SQL rows**, not versioned file trees |
|
||||||
|
|
||||||
|
## Recommended Workflow
|
||||||
|
|
||||||
|
- Create one repo per agent, session, user workspace, or task when work should stay isolated.
|
||||||
|
- Fork from a stable baseline when many repos need the same starter files or prompts.
|
||||||
|
- Use branches only when collaborators share the same lifecycle and need to work in one repo.
|
||||||
|
- Use namespaces to separate environments, teams, or high-rate workloads.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
**From a Worker:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Env {
|
||||||
|
ARTIFACTS: Artifacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await env.ARTIFACTS.create("starter-repo");
|
||||||
|
// created.remote -> git remote URL
|
||||||
|
// created.token -> initial repo token
|
||||||
|
```
|
||||||
|
|
||||||
|
**From the REST API:**
|
||||||
|
|
||||||
|
Use the namespace-scoped Artifacts base URL plus a gateway JWT. For imports from existing HTTPS remotes, use the REST API rather than the Workers binding.
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Read |
|
||||||
|
|------|------|
|
||||||
|
| Decide whether Artifacts is the right product | README only |
|
||||||
|
| Create or manage repos from a Worker | README → configuration.md → api.md |
|
||||||
|
| Integrate Artifacts from an external system | README → api.md |
|
||||||
|
| Set up agent or sandbox workflows | README → configuration.md |
|
||||||
|
| Verify exact auth, routes, limits, or pricing | Live docs first: `https://developers.cloudflare.com/artifacts/` |
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- **[api.md](api.md)** - Workers binding methods, REST routes, token and repo operations
|
||||||
|
- **[configuration.md](configuration.md)** - Wrangler binding shape, Worker typing, REST configuration guidance
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Cloudflare Artifacts Docs](https://developers.cloudflare.com/artifacts/)
|
||||||
|
- [Artifacts Git Protocol Docs](https://developers.cloudflare.com/artifacts/api/git-protocol/)
|
||||||
|
- [ArtifactFS Docs](https://developers.cloudflare.com/artifacts/guides/artifact-fs/)
|
||||||
|
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
|
||||||
|
- [Cloudflare Durable Objects Docs](https://developers.cloudflare.com/durable-objects/)
|
||||||
|
- [Cloudflare R2 Docs](https://developers.cloudflare.com/r2/)
|
||||||
|
- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1/)
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Artifacts API Reference
|
||||||
|
|
||||||
|
Use Artifacts through the **Workers binding**, the **REST control plane**, and **Git-compatible remotes**.
|
||||||
|
|
||||||
|
**Prefer retrieval** for exact request and response details. Verify current behavior at `https://developers.cloudflare.com/artifacts/` before relying on specific auth flows, route details, or generated binding types.
|
||||||
|
|
||||||
|
## Workers Binding
|
||||||
|
|
||||||
|
Artifacts exposes a Worker binding on `env.ARTIFACTS`.
|
||||||
|
|
||||||
|
### Namespace Methods
|
||||||
|
|
||||||
|
| Method | Use For |
|
||||||
|
|--------|---------|
|
||||||
|
| `create(name, opts?)` | Create a repo and receive its initial remote and token |
|
||||||
|
| `get(name)` | Resolve a repo handle for repo-scoped operations |
|
||||||
|
| `list(opts?)` | List repos in a namespace |
|
||||||
|
| `delete(name)` | Delete a repo |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const created = await env.ARTIFACTS.create("starter-repo", {
|
||||||
|
description: "Repository for automation experiments",
|
||||||
|
setDefaultBranch: "main"
|
||||||
|
});
|
||||||
|
const repo = await env.ARTIFACTS.get("starter-repo");
|
||||||
|
const page = await env.ARTIFACTS.list({ limit: 10 });
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the REST API when you need to import a repo from another HTTPS remote.
|
||||||
|
|
||||||
|
### Repo Handle Methods
|
||||||
|
|
||||||
|
Use a repo handle returned by `get()` or `create()`.
|
||||||
|
|
||||||
|
| Method | Use For |
|
||||||
|
|--------|---------|
|
||||||
|
| `info()` | Read repo metadata, including the remote URL |
|
||||||
|
| `createToken(scope?, ttl?)` | Mint a repo-scoped read or write token |
|
||||||
|
| `listTokens()` | Inspect active tokens |
|
||||||
|
| `validateToken(token)` | Check whether a token is still valid |
|
||||||
|
| `revokeToken(tokenOrId)` | Revoke a token by ID or value |
|
||||||
|
| `fork(name, opts?)` | Fork one repo into another |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const repo = await env.ARTIFACTS.get("starter-repo");
|
||||||
|
if (!repo) throw new Error("Repo not found");
|
||||||
|
|
||||||
|
const info = await repo.info();
|
||||||
|
const token = await repo.createToken("read", 3600);
|
||||||
|
const forked = await repo.fork("starter-repo-copy", {
|
||||||
|
defaultBranchOnly: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binding Notes
|
||||||
|
|
||||||
|
- Current docs describe the runtime binding surface as `create`, `get`, `list`, `delete`, and repo-handle methods like `info`, `createToken`, and `fork`.
|
||||||
|
- Use `npx wrangler types` in the target project and treat the generated `worker-configuration.d.ts` as the source of truth for that environment.
|
||||||
|
- If generated types appear to expose `import()` or a different `get()` shape, verify the live docs before depending on those methods.
|
||||||
|
|
||||||
|
Verify current runtime behavior in the live docs before depending on methods that are not shown in the Workers binding reference.
|
||||||
|
|
||||||
|
## REST API
|
||||||
|
|
||||||
|
Artifacts currently documents a namespace-scoped control plane:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://artifacts.cloudflare.net/v1/api/namespaces/$ARTIFACTS_NAMESPACE
|
||||||
|
```
|
||||||
|
|
||||||
|
Some deployments also expose an `/edge/v1/api/...` base path. Verify the correct base URL for your environment in the live docs.
|
||||||
|
|
||||||
|
Requests to the standard `/v1/api/...` routes use a **gateway JWT** with Bearer authentication.
|
||||||
|
|
||||||
|
Returned repo tokens authenticate **Git operations** against the repo `remote`. They do not authenticate REST control-plane requests.
|
||||||
|
|
||||||
|
Current docs show the standard Cloudflare v4 response envelope around REST results.
|
||||||
|
|
||||||
|
### Repo Routes
|
||||||
|
|
||||||
|
| Route | Use For |
|
||||||
|
|-------|---------|
|
||||||
|
| `POST /repos` | Create a repo |
|
||||||
|
| `GET /repos` | List repos |
|
||||||
|
| `GET /repos/:name` | Read repo metadata and remote |
|
||||||
|
| `DELETE /repos/:name` | Delete a repo |
|
||||||
|
| `POST /repos/:name/fork` | Fork a repo |
|
||||||
|
| `POST /repos/:name/import` | Import a public HTTPS remote |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST "$ARTIFACTS_BASE_URL/repos" \
|
||||||
|
--header "Authorization: Bearer $ARTIFACTS_JWT" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data '{"name":"starter-repo"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Important current details from the docs draft:
|
||||||
|
- `POST /repos/:name/import` accepts a full HTTPS remote URL such as GitHub or GitLab.
|
||||||
|
- Import supports options such as `branch`, `depth`, and `read_only`.
|
||||||
|
- Repo metadata includes fields such as description, default branch, timestamps, and the Git `remote`.
|
||||||
|
|
||||||
|
### Token Routes
|
||||||
|
|
||||||
|
| Route | Use For |
|
||||||
|
|-------|---------|
|
||||||
|
| `GET /repos/:name/tokens` | List repo tokens |
|
||||||
|
| `POST /tokens` | Create a token for a repo |
|
||||||
|
| `DELETE /tokens/:id` | Revoke a token by ID |
|
||||||
|
|
||||||
|
Current docs show list-token filtering and pagination by token state. Retrieve the exact query shape from the live docs when you need token audit or cleanup workflows.
|
||||||
|
|
||||||
|
Use **read** tokens for clone, fetch, pull, and indexing workflows. Use **write** tokens only when a workflow must push or otherwise mutate a repo.
|
||||||
|
|
||||||
|
## Git-Compatible Access
|
||||||
|
|
||||||
|
Artifacts returns repo `remote` URLs that work with standard git-over-HTTPS tooling.
|
||||||
|
|
||||||
|
Recommended current auth pattern for local workflows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -c http.extraHeader="Authorization: Bearer $ARTIFACTS_TOKEN" clone "$ARTIFACTS_REMOTE" artifacts-clone
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a self-contained Basic-auth remote only for short-lived commands that need credentials embedded in the URL.
|
||||||
|
|
||||||
|
`read` tokens support `clone`, `fetch`, and `pull`. `git push` requires a `write` token.
|
||||||
|
|
||||||
|
For large repos where startup time matters more than a full clone, Artifacts also documents **ArtifactFS**. Retrieve current details from `https://developers.cloudflare.com/artifacts/` when you need mount-style access.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Artifacts Configuration
|
||||||
|
|
||||||
|
## Worker Binding
|
||||||
|
|
||||||
|
Configure the `artifacts` binding in your Wrangler config:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[artifacts]]
|
||||||
|
binding = "ARTIFACTS"
|
||||||
|
namespace = "default"
|
||||||
|
```
|
||||||
|
|
||||||
|
This exposes Artifacts on `env.ARTIFACTS` inside your Worker.
|
||||||
|
|
||||||
|
If you authenticate with `wrangler login`, current docs say Wrangler requests `artifacts:write` by default.
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
Regenerate Worker types after adding the binding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler types
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the generated binding type in your environment definition:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Env {
|
||||||
|
ARTIFACTS: Artifacts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrangler generates the `Artifacts` type from the binding. Treat the generated `worker-configuration.d.ts` file as the source of truth for your environment.
|
||||||
|
|
||||||
|
## Structure Repos for Isolation
|
||||||
|
|
||||||
|
Artifacts works best when autonomous work is isolated:
|
||||||
|
- Create one repo per agent, session, sandbox, or task when work should stay separate.
|
||||||
|
- Fork from a reviewed baseline instead of copying starter files into every new repo.
|
||||||
|
- Use branches only when collaborators share the same lifecycle and need to work in one repo.
|
||||||
|
- Use namespaces to separate environments, teams, or high-rate workloads.
|
||||||
|
|
||||||
|
## REST Configuration
|
||||||
|
|
||||||
|
For external systems, configure the namespace-scoped base URL and gateway JWT:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ARTIFACTS_NAMESPACE="default"
|
||||||
|
export ARTIFACTS_JWT="<YOUR_GATEWAY_JWT>"
|
||||||
|
export ARTIFACTS_BASE_URL="https://artifacts.cloudflare.net/v1/api/namespaces/$ARTIFACTS_NAMESPACE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Some environments also expose an `/edge/v1/api/...` base path. Verify the correct host and base path in the live docs for your Artifacts environment.
|
||||||
|
|
||||||
|
Use environment variables or your secret manager. Do not hardcode gateway JWTs or repo tokens.
|
||||||
|
|
||||||
|
## Repo Tokens
|
||||||
|
|
||||||
|
Artifacts workflows usually involve repo-scoped tokens returned by `create()` or minted later through the binding or REST API.
|
||||||
|
|
||||||
|
Keep the control plane and data plane separate:
|
||||||
|
- Use the **Workers binding** or **REST API** with a gateway JWT to create repos and mint tokens.
|
||||||
|
- Use repo-scoped tokens only for **Git operations** against the returned `remote`.
|
||||||
|
|
||||||
|
Recommended handling:
|
||||||
|
- Mint the narrowest scope you need: `read` or `write`
|
||||||
|
- Prefer short-lived tokens for handoff between systems
|
||||||
|
- Revoke tokens that are no longer needed
|
||||||
|
|
||||||
|
Verify the current token behavior and auth guidance in `https://developers.cloudflare.com/artifacts/` before building long-lived automation.
|
||||||
|
|
||||||
|
## Git Consumers
|
||||||
|
|
||||||
|
Artifacts is designed to work with standard git-over-HTTPS clients once you have a repo `remote` and an access token.
|
||||||
|
|
||||||
|
Prefer header-based auth for local tooling so the full token stays out of the remote URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -c http.extraHeader="Authorization: Bearer $ARTIFACTS_TOKEN" clone "$ARTIFACTS_REMOTE" artifacts-clone
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a Basic-auth remote only for short-lived commands that need a self-contained URL.
|
||||||
|
|
||||||
|
## Retrieval Checklist
|
||||||
|
|
||||||
|
Check the live docs before relying on:
|
||||||
|
- the current Workers binding surface
|
||||||
|
- exact token formats
|
||||||
|
- availability or product status
|
||||||
|
- route details for import, fork, and token-management flows
|
||||||
|
- the correct control-plane host or `/edge/v1` base path for your environment
|
||||||
|
- platform limits or pricing
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cloudflare Workers Bindings
|
||||||
|
|
||||||
|
Bindings grant a Worker access to configured resources through its environment. Prefer a product's binding for supported operations inside Workers; use the REST API when the caller or operation requires it.
|
||||||
|
|
||||||
|
Read the relevant current documentation before implementing. These references route to maintained APIs and configuration rather than copying binding catalogs, type tables, or limits.
|
||||||
|
|
||||||
|
## Start here
|
||||||
|
|
||||||
|
- [Bindings overview and catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/): capability model, available products, environment access, and binding lifecycle.
|
||||||
|
- [Storage options](https://developers.cloudflare.com/workers/platform/storage-options/): choose storage from consistency, query, and coordination requirements.
|
||||||
|
- [api.md](./api.md): environment access, generated types, and product APIs.
|
||||||
|
- [configuration.md](./configuration.md): binding configuration, environments, secrets, and local development.
|
||||||
|
- [patterns.md](./patterns.md): Worker-to-Worker calls, testing, and resource selection.
|
||||||
|
- [gotchas.md](./gotchas.md): missing bindings, stale clients, development differences, and limits.
|
||||||
|
|
||||||
|
Treat each binding as a capability granted to code. Select only the resources the Worker needs, and confirm which environment and resource each binding targets before using it.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Bindings API Reference
|
||||||
|
|
||||||
|
Fetch the relevant documentation before choosing method signatures, binding types, or type-generation settings.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Access bindings in handlers, entrypoint classes, or imported `env` | [Bindings and environment access](https://developers.cloudflare.com/workers/runtime-apis/bindings/) |
|
||||||
|
| Generate environment and runtime types; configure TypeScript or migrate from `@cloudflare/workers-types` | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) |
|
||||||
|
| Use framework-specific environment access | [Hono on Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/) (follow the framework's linked documentation) |
|
||||||
|
| Read, write, delete, and list KV keys | [KV Workers API](https://developers.cloudflare.com/kv/api/) |
|
||||||
|
| Read, write, delete, and list R2 objects | [R2 Workers API](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/) |
|
||||||
|
| Prepare and bind SQL statements or execute batches | [D1 Workers Binding API](https://developers.cloudflare.com/d1/worker-api/) |
|
||||||
|
| Address Durable Objects and call their methods | [Durable Objects API](https://developers.cloudflare.com/durable-objects/api/) |
|
||||||
|
| Send queue messages | [Queues JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/) |
|
||||||
|
| Run model inference | [Workers AI bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/) |
|
||||||
|
| Call another Worker using HTTP or typed RPC | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) and [RPC TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/) |
|
||||||
|
| Find other product binding APIs, including Browser, mTLS, rate limiting, and Workflows | [Current binding catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/) |
|
||||||
|
|
||||||
|
Regenerate types after configuration changes and use the selected environment's configuration. Follow the current TypeScript setup for the project's toolchain instead of hardcoding a generated declaration path or maintaining a handwritten binding interface. Types describe the expected bindings; they do not provision resources or prove the deployed environment is configured correctly.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Binding Configuration
|
||||||
|
|
||||||
|
Confirm the target account, environment, and resource before adding or changing a binding. Keep staging and production resources separate where their data or permissions must be isolated.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Configure storage, compute, platform, and service bindings | [Wrangler binding configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) |
|
||||||
|
| Create or inspect resources and manage deployments | [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/) (select the product's commands) |
|
||||||
|
| Configure named environments and non-inheritable bindings | [Wrangler environments](https://developers.cloudflare.com/workers/wrangler/environments/) |
|
||||||
|
| Set non-sensitive configuration | [Environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) |
|
||||||
|
| Set or rotate credentials in a chosen environment | [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) |
|
||||||
|
| Configure text, data, and Wasm modules in existing projects | [Wrangler bundling](https://developers.cloudflare.com/workers/wrangler/bundling/) and [configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) |
|
||||||
|
| Choose locally simulated resources or remote bindings | [Local development](https://developers.cloudflare.com/workers/local-development/) and [supported bindings per development mode](https://developers.cloudflare.com/workers/local-development/bindings-per-env/) |
|
||||||
|
| Supply local variables and secrets | [Local environment variables and secrets](https://developers.cloudflare.com/workers/local-development/environment-variables/) |
|
||||||
|
| Seed or persist local resource data | [Adding local data](https://developers.cloudflare.com/workers/local-development/local-data/) |
|
||||||
|
| Generate types after configuring bindings | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) |
|
||||||
|
|
||||||
|
Bindings and variables are not inherited automatically by named environments. Configure the required values for the environment being used, including its secrets. Binding names used in code must match configuration; the target resource's ID or name is a separate value.
|
||||||
|
|
||||||
|
Keep credentials out of committed variables and local secret files out of version control. Remote development can access real resources: verify the target rather than assuming that running locally isolates writes.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Binding Gotchas and Troubleshooting
|
||||||
|
|
||||||
|
Start by checking the binding name, selected environment, and actual target resource. Regenerating types alone does not fix a missing runtime binding.
|
||||||
|
|
||||||
|
| Symptom or question | What to check |
|
||||||
|
| --- | --- |
|
||||||
|
| A binding is undefined or points to unexpected data | [Binding configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) and [environment inheritance](https://developers.cloudflare.com/workers/wrangler/environments/) |
|
||||||
|
| TypeScript cannot find a binding or runtime type | [Current type generation and TypeScript setup](https://developers.cloudflare.com/workers/languages/typescript/) |
|
||||||
|
| Updated credentials are not reflected in a client | [Binding changes and retained global state](https://developers.cloudflare.com/workers/runtime-apis/bindings/#making-changes-to-bindings); verify the [secret's environment and deployment](https://developers.cloudflare.com/workers/configuration/secrets/) |
|
||||||
|
| Binding calls fail outside a handler | [Environment access and global-scope restrictions](https://developers.cloudflare.com/workers/runtime-apis/bindings/#how-to-access-env) |
|
||||||
|
| A service target is unavailable or incorrect | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) and [multi-Worker development](https://developers.cloudflare.com/workers/local-development/multi-workers/) |
|
||||||
|
| Local data, secrets, or available bindings differ from deployment | [Development mode support](https://developers.cloudflare.com/workers/local-development/bindings-per-env/), [local data](https://developers.cloudflare.com/workers/local-development/local-data/), and [local secrets](https://developers.cloudflare.com/workers/local-development/environment-variables/) |
|
||||||
|
| KV reads appear stale or return no value | [How KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/); verify the namespace and handle missing values |
|
||||||
|
| Resource-specific API errors or limits | Follow the product from the [binding catalog](https://developers.cloudflare.com/workers/runtime-apis/bindings/) to its troubleshooting and limits documentation |
|
||||||
|
| Worker resource limits or unexpected charges | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) and [pricing](https://developers.cloudflare.com/workers/platform/pricing/); check each bound product's limits and pricing separately |
|
||||||
|
| Need to inspect configuration, resources, deployments, or logs | [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/) and [real-time logs](https://developers.cloudflare.com/workers/observability/logs/real-time-logs/) |
|
||||||
|
|
||||||
|
Never log secret values or return the environment object in a response. Inspect names and configuration without exposing credentials. Check the selected resource and development mode before issuing debugging commands that could mutate remote data.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Binding Patterns
|
||||||
|
|
||||||
|
Choose the interaction and lifecycle first, then retrieve the implementation guide.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Choose HTTP forwarding or RPC between Workers | [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) |
|
||||||
|
| Forward Requests and Responses through a service binding | [Service bindings over HTTP](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/) |
|
||||||
|
| Expose callable methods with `WorkerEntrypoint` | [Service bindings over RPC](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) and [RPC TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/) |
|
||||||
|
| Run connected Workers during development | [Developing with multiple Workers](https://developers.cloudflare.com/workers/local-development/multi-workers/) |
|
||||||
|
| Test handlers against configured bindings and mock dependencies | [Workers Vitest configuration](https://developers.cloudflare.com/workers/testing/vitest-integration/configuration/) and [test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/) |
|
||||||
|
| Select KV, D1, R2, or Durable Objects | [Storage options](https://developers.cloudflare.com/workers/platform/storage-options/) |
|
||||||
|
| Keep clients current when bindings change | [Binding lifecycle](https://developers.cloudflare.com/workers/runtime-apis/bindings/#making-changes-to-bindings) |
|
||||||
|
| Manage credentials used by external API clients | [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) |
|
||||||
|
|
||||||
|
Use service bindings for internal Worker calls when appropriate, and choose HTTP or RPC based on the interface being exposed. A service binding does not replace application-level authorization for the caller's requested operation.
|
||||||
|
|
||||||
|
Choose storage based on access patterns and consistency requirements, not copied size or latency thresholds. Parallelize independent binding operations when useful; preserve ordering where one operation depends on another's result.
|
||||||
|
|
||||||
|
Avoid retaining clients derived from mutable bindings across requests without accounting for binding updates. Importing `env` is supported, but binding I/O still requires an appropriate execution context; follow the lifecycle guide rather than assuming all global access is forbidden.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Cloudflare Bot Management
|
||||||
|
|
||||||
|
Enterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Bot Management provides multi-tier protection:
|
||||||
|
- **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config
|
||||||
|
- **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings
|
||||||
|
- **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Dashboard: Security > Bots
|
||||||
|
# Enterprise: Deploy rule template
|
||||||
|
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block
|
||||||
|
(cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Do You Need?
|
||||||
|
|
||||||
|
```txt
|
||||||
|
├─ Initial setup → configuration.md
|
||||||
|
│ ├─ Free tier → "Bot Fight Mode"
|
||||||
|
│ ├─ Pro/Business → "Super Bot Fight Mode"
|
||||||
|
│ └─ Enterprise → "Bot Management for Enterprise"
|
||||||
|
├─ Workers API integration → api.md
|
||||||
|
├─ WAF rules → patterns.md
|
||||||
|
├─ Debugging → gotchas.md
|
||||||
|
└─ Analytics → api.md#bot-analytics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Files to Read |
|
||||||
|
|------|---------------|
|
||||||
|
| Enable bot protection | README → configuration.md |
|
||||||
|
| Workers bot detection | README → api.md |
|
||||||
|
| WAF rule templates | README → patterns.md |
|
||||||
|
| Debug bot issues | gotchas.md |
|
||||||
|
| Advanced analytics | api.md#bot-analytics |
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
**Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only.
|
||||||
|
|
||||||
|
**Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection).
|
||||||
|
|
||||||
|
**Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`.
|
||||||
|
|
||||||
|
## Platform Limits
|
||||||
|
|
||||||
|
| Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention |
|
||||||
|
|------|------------|---------|--------------|---------------------|
|
||||||
|
| Free | No (auto-block only) | No | 5 | N/A (no analytics) |
|
||||||
|
| Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) |
|
||||||
|
| Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) |
|
||||||
|
|
||||||
|
## Basic Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Workers: Check bot score
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const botScore = request.cf?.botManagement?.score;
|
||||||
|
if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) {
|
||||||
|
return new Response('Bot detected', { status: 403 });
|
||||||
|
}
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# WAF: Block definite bots
|
||||||
|
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot)
|
||||||
|
|
||||||
|
# WAF: Protect sensitive endpoints
|
||||||
|
(cf.bot_management.score lt 50 and http.request.uri.path in {"/login" "/checkout"} and not cf.bot_management.verified_bot)
|
||||||
|
```
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates
|
||||||
|
- [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals
|
||||||
|
- [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling
|
||||||
|
- [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [waf](../waf/) - WAF custom rules for bot enforcement
|
||||||
|
- [workers](https://developers.cloudflare.com/workers/) - Workers request.cf.botManagement API
|
||||||
|
- [api-shield](../api-shield/) - API-specific bot protection
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Bot Management API
|
||||||
|
|
||||||
|
## Workers: BotManagement Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BotManagement {
|
||||||
|
score: number; // 1-99 (Enterprise), 0 if not computed
|
||||||
|
verifiedBot: boolean; // Is verified bot
|
||||||
|
staticResource: boolean; // Serves static resource
|
||||||
|
ja3Hash: string; // JA3 fingerprint (Enterprise, HTTPS only)
|
||||||
|
ja4: string; // JA4 fingerprint (Enterprise, HTTPS only)
|
||||||
|
jsDetection?: {
|
||||||
|
passed: boolean; // Passed JS detection (if enabled)
|
||||||
|
};
|
||||||
|
detectionIds: number[]; // Heuristic detection IDs
|
||||||
|
corporateProxy?: boolean; // From corporate proxy (Enterprise)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEPRECATED: Use botManagement.score instead
|
||||||
|
// request.cf.clientTrustScore (legacy, duplicate of botManagement.score)
|
||||||
|
|
||||||
|
// Access via request.cf
|
||||||
|
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||||
|
const botMgmt = cf?.botManagement;
|
||||||
|
|
||||||
|
if (!botMgmt) return fetch(request);
|
||||||
|
if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots
|
||||||
|
if (botMgmt.score === 1) return new Response('Blocked', { status: 403 });
|
||||||
|
if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 });
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## WAF Fields Reference
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Score fields
|
||||||
|
cf.bot_management.score # 0-99 (0 = not computed)
|
||||||
|
cf.bot_management.verified_bot # boolean
|
||||||
|
cf.bot_management.static_resource # boolean
|
||||||
|
cf.bot_management.ja3_hash # string (Enterprise)
|
||||||
|
cf.bot_management.ja4 # string (Enterprise)
|
||||||
|
cf.bot_management.detection_ids # array
|
||||||
|
cf.bot_management.js_detection.passed # boolean
|
||||||
|
cf.bot_management.corporate_proxy # boolean (Enterprise)
|
||||||
|
cf.verified_bot_category # string
|
||||||
|
|
||||||
|
# Workers equivalent
|
||||||
|
request.cf.botManagement.score
|
||||||
|
request.cf.botManagement.verifiedBot
|
||||||
|
request.cf.botManagement.ja3Hash
|
||||||
|
request.cf.botManagement.ja4
|
||||||
|
request.cf.botManagement.jsDetection.passed
|
||||||
|
request.cf.verifiedBotCategory
|
||||||
|
```
|
||||||
|
|
||||||
|
## JA4 Signals (Enterprise)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
|
interface JA4Signals {
|
||||||
|
// Ratios (0.0-1.0)
|
||||||
|
heuristic_ratio_1h?: number; // Fraction flagged by heuristics
|
||||||
|
browser_ratio_1h?: number; // Fraction from real browsers
|
||||||
|
cache_ratio_1h?: number; // Fraction hitting cache
|
||||||
|
h2h3_ratio_1h?: number; // Fraction using HTTP/2 or HTTP/3
|
||||||
|
// Ranks (relative position in distribution)
|
||||||
|
uas_rank_1h?: number; // User-Agent diversity rank
|
||||||
|
paths_rank_1h?: number; // Path diversity rank
|
||||||
|
reqs_rank_1h?: number; // Request volume rank
|
||||||
|
ips_rank_1h?: number; // IP diversity rank
|
||||||
|
// Quantiles (0.0-1.0, percentile in distribution)
|
||||||
|
reqs_quantile_1h?: number; // Request volume quantile
|
||||||
|
ips_quantile_1h?: number; // IP count quantile
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||||
|
const ja4Signals = cf?.ja4Signals as JA4Signals | undefined;
|
||||||
|
|
||||||
|
if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing
|
||||||
|
|
||||||
|
// Check for anomalous behavior
|
||||||
|
// High heuristic_ratio or low browser_ratio = suspicious
|
||||||
|
const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0;
|
||||||
|
const browserRatio = ja4Signals.browser_ratio_1h ?? 0;
|
||||||
|
|
||||||
|
if (heuristicRatio > 0.5 || browserRatio < 0.3) {
|
||||||
|
return new Response('Suspicious traffic', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
See [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more.
|
||||||
|
|
||||||
|
## Bot Analytics
|
||||||
|
|
||||||
|
### Access Locations
|
||||||
|
- Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new)
|
||||||
|
- GraphQL API for programmatic access
|
||||||
|
- Security Events & Security Analytics
|
||||||
|
- Logpush/Logpull
|
||||||
|
|
||||||
|
### Available Data
|
||||||
|
- **Enterprise BM**: Bot scores (1-99), bot score source, distribution
|
||||||
|
- **Pro/Business**: Bot groupings (automated, likely automated, likely human)
|
||||||
|
- Top attributes: IPs, paths, user agents, countries
|
||||||
|
- Detection sources: Heuristics, ML, AD, JSD
|
||||||
|
- Verified bot categories
|
||||||
|
|
||||||
|
### Time Ranges
|
||||||
|
- **Enterprise BM**: Up to 1 week at a time, 30 days history
|
||||||
|
- **Pro/Business**: Up to 72 hours at a time, 30 days history
|
||||||
|
- Real-time in most cases, adaptive sampling (1-10% depending on volume)
|
||||||
|
|
||||||
|
## Logpush Fields
|
||||||
|
|
||||||
|
```txt
|
||||||
|
BotScore # 1-99 or 0 if not computed
|
||||||
|
BotScoreSrc # Detection engine (ML, Heuristics, etc.)
|
||||||
|
BotTags # Classification tags
|
||||||
|
BotDetectionIDs # Heuristic detection IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
**BotScoreSrc values:**
|
||||||
|
- `"Heuristics"` - Known fingerprint
|
||||||
|
- `"Machine Learning"` - ML model
|
||||||
|
- `"Anomaly Detection"` - Baseline anomaly
|
||||||
|
- `"JS Detection"` - JavaScript check
|
||||||
|
- `"Cloudflare Service"` - Zero Trust
|
||||||
|
- `"Not Computed"` - Score = 0
|
||||||
|
|
||||||
|
Access via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data).
|
||||||
|
|
||||||
|
## Testing with Miniflare
|
||||||
|
|
||||||
|
Miniflare provides mock botManagement data for local development:
|
||||||
|
|
||||||
|
**Default values:**
|
||||||
|
- `score: 99` (human)
|
||||||
|
- `verifiedBot: false`
|
||||||
|
- `corporateProxy: false`
|
||||||
|
- `ja3Hash: "25b4882c2bcb50cd6b469ff28c596742"`
|
||||||
|
- `staticResource: false`
|
||||||
|
- `detectionIds: []`
|
||||||
|
|
||||||
|
**Override in tests:**
|
||||||
|
```typescript
|
||||||
|
import { getPlatformProxy } from 'wrangler';
|
||||||
|
|
||||||
|
const { cf, dispose } = await getPlatformProxy();
|
||||||
|
// cf.botManagement is frozen mock object
|
||||||
|
expect(cf.botManagement.score).toBe(99);
|
||||||
|
```
|
||||||
|
|
||||||
|
For custom test data, mock request.cf in your test setup.
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Bot Management Configuration
|
||||||
|
|
||||||
|
## Product Tiers
|
||||||
|
|
||||||
|
**Note:** Dashboard paths differ between old and new UI:
|
||||||
|
- **New:** Security > Settings > Filter "Bot traffic"
|
||||||
|
- **Old:** Security > Bots
|
||||||
|
|
||||||
|
Both UIs access same settings.
|
||||||
|
|
||||||
|
### Bot Score Groupings (Pro/Business)
|
||||||
|
|
||||||
|
Pro/Business users see bot score groupings instead of granular 1-99 scores:
|
||||||
|
|
||||||
|
| Score | Grouping | Meaning |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| 0 | Not computed | Bot Management didn't run |
|
||||||
|
| 1 | Automated | Definite bot (heuristic match) |
|
||||||
|
| 2-29 | Likely automated | Probably bot (ML detection) |
|
||||||
|
| 30-99 | Likely human | Probably human |
|
||||||
|
| N/A | Verified bot | Allowlisted good bot |
|
||||||
|
|
||||||
|
Enterprise plans get granular 1-99 scores for custom thresholds.
|
||||||
|
|
||||||
|
### Bot Fight Mode (Free)
|
||||||
|
- Auto-blocks definite bots (score=1), excludes verified bots by default
|
||||||
|
- JavaScript Detections always enabled, no configuration options
|
||||||
|
|
||||||
|
### Super Bot Fight Mode (Pro/Business)
|
||||||
|
```txt
|
||||||
|
Dashboard: Security > Bots > Configure
|
||||||
|
- Definitely automated: Block/Challenge
|
||||||
|
- Likely automated: Challenge/Allow
|
||||||
|
- Verified bots: Allow (recommended)
|
||||||
|
- Static resource protection: ON (may block mail clients)
|
||||||
|
- JavaScript Detections: Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot Management for Enterprise
|
||||||
|
```txt
|
||||||
|
Dashboard: Security > Bots > Configure > Auto-updates: ON (recommended)
|
||||||
|
|
||||||
|
# Template 1: Block definite bots
|
||||||
|
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
|
||||||
|
Action: Block
|
||||||
|
|
||||||
|
# Template 2: Challenge likely bots
|
||||||
|
(cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
|
||||||
|
Action: Managed Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript Detections Setup
|
||||||
|
|
||||||
|
### Enable via Dashboard
|
||||||
|
```txt
|
||||||
|
Security > Bots > Configure Bot Management > JS Detections: ON
|
||||||
|
|
||||||
|
Update CSP: script-src 'self' /cdn-cgi/challenge-platform/;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual JS Injection (API)
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
function jsdOnload() {
|
||||||
|
window.cloudflare.jsd.executeOnce({ callback: function(result) { console.log('JSD:', result); } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="/cdn-cgi/challenge-platform/scripts/jsd/api.js?onload=jsdOnload" async></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use API for**: Selective deployment on specific pages
|
||||||
|
**Don't combine**: Zone-wide toggle + manual injection
|
||||||
|
|
||||||
|
### WAF Rules for JSD
|
||||||
|
```txt
|
||||||
|
# NEVER use on first page visit (needs HTML page first)
|
||||||
|
(not cf.bot_management.js_detection.passed and http.request.uri.path eq "/api/user/create" and http.request.method eq "POST" and not cf.bot_management.verified_bot)
|
||||||
|
Action: Managed Challenge (always use Managed Challenge, not Block)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
- First request won't have JSD data (needs HTML page first)
|
||||||
|
- Strips ETags from HTML responses
|
||||||
|
- Not supported with CSP via `<meta>` tags
|
||||||
|
- Websocket endpoints not supported
|
||||||
|
- Native mobile apps won't pass
|
||||||
|
- cf_clearance cookie: 15-minute lifespan, max 4096 bytes
|
||||||
|
|
||||||
|
## __cf_bm Cookie
|
||||||
|
|
||||||
|
Cloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions:
|
||||||
|
|
||||||
|
- **Purpose:** Reduces false positives from score volatility
|
||||||
|
- **Scope:** Per-domain, HTTP-only
|
||||||
|
- **Lifespan:** Session duration
|
||||||
|
- **Privacy:** No PII—only session classification
|
||||||
|
- **Automatic:** No configuration required
|
||||||
|
|
||||||
|
Bot scores for repeat visitors consider session history via this cookie.
|
||||||
|
|
||||||
|
## Static Resource Protection
|
||||||
|
|
||||||
|
**File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts
|
||||||
|
**Plus**: `/.well-known/` path (all files)
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Exclude static resources from bot rules
|
||||||
|
(cf.bot_management.score lt 30 and not cf.bot_management.static_resource)
|
||||||
|
```
|
||||||
|
|
||||||
|
**WARNING**: May block mail clients fetching static images
|
||||||
|
|
||||||
|
## JA3/JA4 Fingerprinting (Enterprise)
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Block specific attack fingerprint
|
||||||
|
(cf.bot_management.ja3_hash eq "8b8e3d5e3e8b3d5e")
|
||||||
|
|
||||||
|
# Allow mobile app by fingerprint
|
||||||
|
(cf.bot_management.ja4 eq "your_mobile_app_fingerprint")
|
||||||
|
```
|
||||||
|
|
||||||
|
Only available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests.
|
||||||
|
|
||||||
|
## Verified Bot Categories
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Allow search engines only
|
||||||
|
(cf.verified_bot_category eq "Search Engine Crawler")
|
||||||
|
|
||||||
|
# Block AI crawlers
|
||||||
|
(cf.verified_bot_category eq "AI Crawler")
|
||||||
|
Action: Block
|
||||||
|
|
||||||
|
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
|
||||||
|
```
|
||||||
|
|
||||||
|
| Category | String Value | Example |
|
||||||
|
|----------|--------------|---------|
|
||||||
|
| AI Crawler | `AI Crawler` | GPTBot, Claude-Web |
|
||||||
|
| AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot |
|
||||||
|
| AI Search | `AI Search` | OAI-SearchBot |
|
||||||
|
| Accessibility | `Accessibility` | Accessible Web Bot |
|
||||||
|
| Academic Research | `Academic Research` | Library of Congress |
|
||||||
|
| Advertising & Marketing | `Advertising & Marketing` | Google Adsbot |
|
||||||
|
| Aggregator | `Aggregator` | Pinterest, Indeed |
|
||||||
|
| Archiver | `Archiver` | Internet Archive, CommonCrawl |
|
||||||
|
| Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters |
|
||||||
|
| Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors |
|
||||||
|
| Page Preview | `Page Preview` | Facebook/Slack link preview |
|
||||||
|
| SEO | `Search Engine Optimization` | Google Lighthouse |
|
||||||
|
| Security | `Security` | Vulnerability scanners |
|
||||||
|
| Social Media Marketing | `Social Media Marketing` | Brandwatch |
|
||||||
|
| Webhooks | `Webhooks` | Payment processors |
|
||||||
|
| Other | `Other` | Uncategorized bots |
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- **ML Auto-Updates**: Enable on Enterprise for latest models
|
||||||
|
- **Start with Managed Challenge**: Test before blocking
|
||||||
|
- **Always exclude verified bots**: Use `not cf.bot_management.verified_bot`
|
||||||
|
- **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy`
|
||||||
|
- **Use static resource exception**: Improves performance, reduces overhead
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Bot Management Gotchas
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### "Bot Score = 0"
|
||||||
|
|
||||||
|
**Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.))
|
||||||
|
**Solution:** Check request flow and ensure Bot Management runs in request lifecycle
|
||||||
|
|
||||||
|
### "JavaScript Detections Not Working"
|
||||||
|
|
||||||
|
**Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge)
|
||||||
|
**Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action
|
||||||
|
|
||||||
|
### "False Positives (Legitimate Users Blocked)"
|
||||||
|
|
||||||
|
**Cause:** Bot detection incorrectly flagging legitimate users
|
||||||
|
**Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq "/problematic-path")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country
|
||||||
|
|
||||||
|
### "False Negatives (Bots Not Caught)"
|
||||||
|
|
||||||
|
**Cause:** Bots bypassing detection
|
||||||
|
**Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback
|
||||||
|
|
||||||
|
### "Verified Bot Blocked"
|
||||||
|
|
||||||
|
**Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management)
|
||||||
|
**Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS
|
||||||
|
|
||||||
|
### "Yandex Bot Blocked During IP Update"
|
||||||
|
|
||||||
|
**Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation
|
||||||
|
**Solution:**
|
||||||
|
1. Check Security Events for specific WAF rule ID blocking Yandex
|
||||||
|
2. Create WAF exception:
|
||||||
|
```txt
|
||||||
|
(http.user_agent contains "YandexBot" and ip.src in {<yandex-ip-range>})
|
||||||
|
Action: Skip (WAF Managed Ruleset)
|
||||||
|
```
|
||||||
|
3. Monitor Bot Analytics for 48h
|
||||||
|
4. Remove exception after propagation completes
|
||||||
|
|
||||||
|
Issue resolves automatically after 48h. Contact Cloudflare Support if persists.
|
||||||
|
|
||||||
|
### "JA3/JA4 Missing"
|
||||||
|
|
||||||
|
**Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped
|
||||||
|
**Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing
|
||||||
|
|
||||||
|
**JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint
|
||||||
|
- Don't use for user identification
|
||||||
|
- Use for client profiling only
|
||||||
|
- Fingerprints change with browser updates
|
||||||
|
|
||||||
|
## Bot Verification Methods
|
||||||
|
|
||||||
|
Cloudflare verifies bots via:
|
||||||
|
|
||||||
|
1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain
|
||||||
|
2. **Web Bot Auth:** Modern cryptographic verification—faster propagation
|
||||||
|
|
||||||
|
When `verifiedBot=true`, bot passed at least one method.
|
||||||
|
|
||||||
|
**Inactive verified bots:** IPs removed after 24h of no traffic.
|
||||||
|
|
||||||
|
## Detection Engine Behavior
|
||||||
|
|
||||||
|
| Engine | Score | Timing | Plan | Notes |
|
||||||
|
|--------|-------|--------|------|-------|
|
||||||
|
| Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML |
|
||||||
|
| ML | 1-99 | Immediate | All | Majority of detections |
|
||||||
|
| Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis |
|
||||||
|
| JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection |
|
||||||
|
| Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source |
|
||||||
|
|
||||||
|
**Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML.
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Limit | Value | Notes |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| Bot Score = 0 | Means not computed | Not score = 100 |
|
||||||
|
| First request JSD data | May not be available | JSD data appears on subsequent requests |
|
||||||
|
| Score accuracy | Not 100% guaranteed | False positives/negatives possible |
|
||||||
|
| JSD on first HTML page visit | Not supported | Requires subsequent page load |
|
||||||
|
| JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers |
|
||||||
|
| JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior |
|
||||||
|
| JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations |
|
||||||
|
| JSD meta CSP tags | Not supported | Must use HTTP headers |
|
||||||
|
| JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD |
|
||||||
|
| JSD mobile app support | Native apps won't pass | Only works in browsers |
|
||||||
|
| JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic |
|
||||||
|
| JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing |
|
||||||
|
| JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library |
|
||||||
|
| JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints |
|
||||||
|
| WAF custom rules (Free) | 5 | Varies by plan |
|
||||||
|
| WAF custom rules (Pro) | 20 | Varies by plan |
|
||||||
|
| WAF custom rules (Business) | 100 | Varies by plan |
|
||||||
|
| WAF custom rules (Enterprise) | 1,000+ | Varies by plan |
|
||||||
|
| Workers CPU time | Varies by plan | Applies to bot logic |
|
||||||
|
| Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively |
|
||||||
|
| Bot Analytics history | 30 days max | Historical data retention limit |
|
||||||
|
| CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function |
|
||||||
|
|
||||||
|
### Plan Restrictions
|
||||||
|
|
||||||
|
| Feature | Free | Pro/Business | Enterprise |
|
||||||
|
|---------|------|--------------|------------|
|
||||||
|
| Granular scores (1-99) | No | No | Yes |
|
||||||
|
| JA3/JA4 | No | No | Yes |
|
||||||
|
| Anomaly Detection | No | No | Yes |
|
||||||
|
| Corporate Proxy detection | No | No | Yes |
|
||||||
|
| Verified bot categories | Limited | Limited | Full |
|
||||||
|
| Custom WAF rules | 5 | 20/100 | 1,000+ |
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# Bot Management Patterns
|
||||||
|
|
||||||
|
## E-commerce Protection
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# High security for checkout
|
||||||
|
(cf.bot_management.score lt 50 and http.request.uri.path in {"/checkout" "/cart/add"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy)
|
||||||
|
Action: Managed Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Protection
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Protect API with JS detection + score
|
||||||
|
(http.request.uri.path matches "^/api/" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot)
|
||||||
|
Action: Block
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEO-Friendly Bot Handling
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Allow search engine crawlers
|
||||||
|
(cf.bot_management.score lt 30 and not cf.verified_bot_category in {"Search Engine Crawler"})
|
||||||
|
Action: Managed Challenge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Block AI Scrapers
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Block training crawlers only (allow AI assistants/search)
|
||||||
|
(cf.verified_bot_category eq "AI Crawler")
|
||||||
|
Action: Block
|
||||||
|
|
||||||
|
# Block all AI-related bots (training + assistants + search)
|
||||||
|
(cf.verified_bot_category in {"AI Crawler" "AI Assistant" "AI Search"})
|
||||||
|
Action: Block
|
||||||
|
|
||||||
|
# Allow AI Search, block AI Crawler and AI Assistant
|
||||||
|
(cf.verified_bot_category in {"AI Crawler" "AI Assistant"})
|
||||||
|
Action: Block
|
||||||
|
|
||||||
|
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting by Bot Score
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Stricter limits for suspicious traffic
|
||||||
|
(cf.bot_management.score lt 50)
|
||||||
|
Rate: 10 requests per 10 seconds
|
||||||
|
|
||||||
|
(cf.bot_management.score ge 50)
|
||||||
|
Rate: 100 requests per 10 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mobile App Allowlisting
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Identify mobile app by JA3/JA4
|
||||||
|
(cf.bot_management.ja4 in {"fingerprint1" "fingerprint2"})
|
||||||
|
Action: Skip (all remaining rules)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datacenter Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
|
// Low score + not corporate proxy = likely datacenter bot
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||||
|
const botMgmt = cf?.botManagement;
|
||||||
|
|
||||||
|
if (botMgmt?.score && botMgmt.score < 30 &&
|
||||||
|
!botMgmt.corporateProxy && !botMgmt.verifiedBot) {
|
||||||
|
return new Response('Datacenter traffic blocked', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional Delay (Tarpit)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
|
// Add delay proportional to bot suspicion
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||||
|
const botMgmt = cf?.botManagement;
|
||||||
|
|
||||||
|
if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) {
|
||||||
|
// Delay: 0-2 seconds for scores 50-0
|
||||||
|
const delayMs = Math.max(0, (50 - botMgmt.score) * 40);
|
||||||
|
await new Promise(r => setTimeout(r, delayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layered Defense
|
||||||
|
|
||||||
|
```txt
|
||||||
|
1. Bot Management (score-based)
|
||||||
|
2. JavaScript Detections (for JS-capable clients)
|
||||||
|
3. Rate Limiting (fallback protection)
|
||||||
|
4. WAF Managed Rules (OWASP, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progressive Enhancement
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Public content: High threshold (score < 10)
|
||||||
|
Authenticated: Medium threshold (score < 30)
|
||||||
|
Sensitive: Low threshold (score < 50) + JSD
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zero Trust for Bots
|
||||||
|
|
||||||
|
```txt
|
||||||
|
1. Default deny (all scores < 30)
|
||||||
|
2. Allowlist verified bots
|
||||||
|
3. Allowlist mobile apps (JA3/JA4)
|
||||||
|
4. Allowlist corporate proxies
|
||||||
|
5. Allowlist static resources
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workers: Score + JS Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||||
|
const botMgmt = cf?.botManagement;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (botMgmt?.staticResource) return fetch(request); // Skip static
|
||||||
|
|
||||||
|
// API endpoints: require JS detection + good score
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false;
|
||||||
|
const score = botMgmt?.score ?? 100;
|
||||||
|
|
||||||
|
if (!jsDetectionPassed || score < 30) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting by JWT Claim + Bot Score
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Enterprise: Combine bot score with JWT validation
|
||||||
|
Rate limiting > Custom rules
|
||||||
|
- Field: lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
|
||||||
|
- Matches: user ID claim
|
||||||
|
- Additional condition: cf.bot_management.score lt 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## WAF Integration Points
|
||||||
|
|
||||||
|
- **WAF Custom Rules**: Primary enforcement mechanism
|
||||||
|
- **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores
|
||||||
|
- **Transform Rules**: Pass score to origin via custom header
|
||||||
|
- **Workers**: Programmatic bot logic, custom scoring algorithms
|
||||||
|
- **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Browser Run (formerly Browser Rendering)
|
||||||
|
|
||||||
|
Use Browser Run for screenshots, PDFs, rendered content extraction, and browser automation. Read the relevant current documentation before implementing; use the [documentation index](https://developers.cloudflare.com/browser-run/llms.txt) to discover additional guides.
|
||||||
|
|
||||||
|
Choose the integration by the work and runtime:
|
||||||
|
|
||||||
|
- For a self-contained screenshot, PDF, or extraction, start with Quick Actions. They are available through REST and Workers bindings; check the chosen action's supported interface.
|
||||||
|
- For multi-step interactions or persistent state, use browser sessions. In Workers, use Cloudflare's Puppeteer or Playwright package; from external scripts or CI, use the CDP integration.
|
||||||
|
- When adapting existing automation, preserve its library where supported and check installed versions against the corresponding guide.
|
||||||
|
|
||||||
|
Read only the reference needed for the task:
|
||||||
|
|
||||||
|
| Task | Reference |
|
||||||
|
|------|-----------|
|
||||||
|
| Set up bindings, dependencies, or development | [configuration.md](configuration.md) |
|
||||||
|
| Select an endpoint or browser client API | [api.md](api.md) |
|
||||||
|
| Implement a workflow or manage reusable sessions | [patterns.md](patterns.md) |
|
||||||
|
| Diagnose failures or plan capacity and cost | [gotchas.md](gotchas.md) |
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Browser Run APIs
|
||||||
|
|
||||||
|
Read the guide for the chosen interface for request schemas, return types, authentication, and supported options. Keep Quick Actions and browser session APIs distinct when adapting examples.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Screenshots, PDFs, HTML, scraping, or structured extraction | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) — links to each action's request options and examples for REST or Workers bindings |
|
||||||
|
| Automate a browser in Workers with Puppeteer | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) — Cloudflare package, browser operations, and session APIs |
|
||||||
|
| Automate a browser in Workers with Playwright | [Playwright](https://developers.cloudflare.com/browser-run/playwright/) — Cloudflare package, locators, storage state, and tracing |
|
||||||
|
| Control a remote browser from an external runtime | [CDP](https://developers.cloudflare.com/browser-run/cdp/) — session endpoints and links to Puppeteer, Playwright, and other clients |
|
||||||
|
|
||||||
|
The product rename does not imply a rename of API paths or token permissions. Use the identifiers shown in the selected guide.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Browser Run Configuration
|
||||||
|
|
||||||
|
Check the project's runtime, installed client and Wrangler versions, and compatibility date before adapting setup instructions. Cloudflare's packages for Workers and standard clients connecting over CDP have different setup requirements.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Start a project or configure REST authentication | [Get started](https://developers.cloudflare.com/browser-run/get-started/) — Quick Actions and browser session setup |
|
||||||
|
| Configure a Worker or choose a development mode | [Wrangler reference](https://developers.cloudflare.com/browser-run/reference/wrangler/) — browser bindings, compatibility requirements, and local/remote development |
|
||||||
|
| Install or update a Workers browser client | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-run/playwright/) — package-specific setup and supported versions |
|
||||||
|
| Connect from a script, server, or CI outside Workers | [CDP](https://developers.cloudflare.com/browser-run/cdp/) — authentication and client integration guides |
|
||||||
|
|
||||||
|
Development support depends on the selected interface. Follow its current guidance rather than applying one remote-mode requirement to all Browser Run workflows.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Browser Run Troubleshooting
|
||||||
|
|
||||||
|
Identify the integration and observed failure before changing timeouts or concurrency. A request-rate limit, exhausted browser time, and a closed session require different responses.
|
||||||
|
|
||||||
|
| Concern | Documentation |
|
||||||
|
|---------|---------------|
|
||||||
|
| Quotas, launch rates, concurrency, and session timeouts | [Limits](https://developers.cloudflare.com/browser-run/limits/) — check the current plan and integration-specific limits |
|
||||||
|
| Browser hours and concurrent-browser charges | [Pricing](https://developers.cloudflare.com/browser-run/pricing/) — distinguish Quick Actions from browser sessions |
|
||||||
|
| Missing bindings, action failures, or unsupported behavior | [FAQ](https://developers.cloudflare.com/browser-run/faq/) — diagnose the reported error and runtime constraints |
|
||||||
|
| Puppeteer page evaluation cannot access outer variables | [JavaScript execution](https://pptr.dev/guides/javascript-execution) — browser execution context, passing arguments, and returned values |
|
||||||
|
| Block resources or handle intercepted Puppeteer requests | [Request interception](https://pptr.dev/guides/network-interception) — continue, respond, or abort requests and avoid duplicate handling |
|
||||||
|
| Unexpected disconnects or session loss | [Browser close reasons](https://developers.cloudflare.com/browser-run/reference/browser-close-reasons/) — inspect the recorded close reason before choosing recovery |
|
||||||
|
| Development or compatibility failures | [Wrangler reference](https://developers.cloudflare.com/browser-run/reference/wrangler/) — verify binding configuration and interface-specific development support |
|
||||||
|
|
||||||
|
Before increasing concurrency, check session cleanup and whether the workload can reuse browsers with appropriate isolation; see [patterns.md](patterns.md). Retrieve current limits and pricing when sizing a workload rather than relying on fixed tier tables.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Browser Run Patterns
|
||||||
|
|
||||||
|
Use the current examples for the selected integration instead of translating between Puppeteer, Playwright, and Quick Actions by changing method names.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Implement screenshots, PDFs, or extraction | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) — choose the action and follow its example |
|
||||||
|
| Build custom interactions | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) or [Playwright](https://developers.cloudflare.com/browser-run/playwright/) — browser automation examples |
|
||||||
|
| Reconnect across requests | [Reuse sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) — disconnect/reconnect lifecycle and when to use Durable Objects for stateful ownership |
|
||||||
|
| Share browser capacity while isolating users | [Concurrency and session isolation](https://developers.cloudflare.com/browser-run/limits/#how-can-i-manage-concurrency-and-session-isolation-with-browser-run) — tabs, browser contexts, and capacity tradeoffs |
|
||||||
|
|
||||||
|
Quick Actions manage their own session lifecycle. For sessions managed by the application, close pages and browsers on completion or failure. If reuse is intentional, follow the client's disconnect/reconnect semantics and handle expired sessions; closing the browser ends it. Keep cookies and storage isolated between users, and coordinate ownership when several requests can reconnect to the same session.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# C3 (create-cloudflare)
|
||||||
|
|
||||||
|
Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive (recommended for first-time)
|
||||||
|
npm create cloudflare@latest my-app
|
||||||
|
|
||||||
|
# Worker (API/WebSocket/Cron)
|
||||||
|
npm create cloudflare@latest my-api -- --type=hello-world --ts
|
||||||
|
|
||||||
|
# Pages (static/SSG)
|
||||||
|
npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
What are you building?
|
||||||
|
|
||||||
|
├─ API / WebSocket / Cron / Email handler
|
||||||
|
│ └─ Workers (default) - no --platform flag needed
|
||||||
|
│ npm create cloudflare@latest my-api -- --type=hello-world
|
||||||
|
|
||||||
|
├─ Static site / SSG / Documentation
|
||||||
|
│ └─ Pages - requires --platform=pages
|
||||||
|
│ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
|
||||||
|
|
||||||
|
├─ Full-stack app (Next.js/Remix/SvelteKit)
|
||||||
|
│ └─ Follow the current framework guide below
|
||||||
|
|
||||||
|
└─ Convert existing project
|
||||||
|
└─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers.
|
||||||
|
|
||||||
|
## Framework Setup
|
||||||
|
|
||||||
|
Fetch the [Workers framework guide](https://developers.cloudflare.com/workers/framework-guides/) for the chosen framework before scaffolding or adapting an existing app. For Next.js, follow [Next.js on Workers](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/); use the [Pages static export guide](https://developers.cloudflare.com/pages/framework-guides/nextjs/deploy-a-static-nextjs-site/) only when targeting a Next.js static export on Pages.
|
||||||
|
|
||||||
|
## Interactive Flow
|
||||||
|
|
||||||
|
When run without flags, C3 prompts in this order:
|
||||||
|
|
||||||
|
1. **Project name** - Directory to create (defaults to current dir with `.`)
|
||||||
|
2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template`
|
||||||
|
3. **Platform** - `workers` (default) or `pages` (for web apps only)
|
||||||
|
4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc.
|
||||||
|
5. **TypeScript** - `yes` (recommended) or `no`
|
||||||
|
6. **Git** - Initialize repository? `yes` or `no`
|
||||||
|
7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`)
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NPM
|
||||||
|
npm create cloudflare@latest
|
||||||
|
|
||||||
|
# Yarn
|
||||||
|
yarn create cloudflare
|
||||||
|
|
||||||
|
# PNPM
|
||||||
|
pnpm create cloudflare@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
| File | Purpose | Use When |
|
||||||
|
|------|---------|----------|
|
||||||
|
| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage |
|
||||||
|
| **configuration.md** | Generated files, bindings, types | Understanding output, customization |
|
||||||
|
| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration |
|
||||||
|
| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors |
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| Task | Read |
|
||||||
|
|------|------|
|
||||||
|
| Create first project | README only |
|
||||||
|
| Set up CI/CD | README → api → patterns |
|
||||||
|
| Debug failed deploy | gotchas |
|
||||||
|
| Understand generated files | configuration |
|
||||||
|
| Full CLI reference | api |
|
||||||
|
| Create custom template | patterns → configuration |
|
||||||
|
| Convert existing project | README → patterns |
|
||||||
|
|
||||||
|
## Post-Creation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd my-app
|
||||||
|
|
||||||
|
# Local dev with hot reload
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Generate TypeScript types for bindings
|
||||||
|
npm run cf-typegen
|
||||||
|
|
||||||
|
# Deploy to Cloudflare
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- **workers/README.md** - Workers runtime, bindings, APIs
|
||||||
|
- **workers-ai/README.md** - AI/ML models
|
||||||
|
- **pages/README.md** - Pages-specific features
|
||||||
|
- **wrangler/README.md** - Wrangler CLI beyond initial setup
|
||||||
|
- **d1/README.md** - SQLite database
|
||||||
|
- **r2/README.md** - Object storage
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# C3 CLI Reference
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create cloudflare@latest [name] [-- flags] # NPM requires --
|
||||||
|
yarn create cloudflare [name] [flags]
|
||||||
|
pnpm create cloudflare@latest [name] [-- flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Flags
|
||||||
|
|
||||||
|
| Flag | Values | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type |
|
||||||
|
| `--platform` | `workers` (default), `pages` | Target platform |
|
||||||
|
| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) |
|
||||||
|
| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) |
|
||||||
|
| `--ts` / `--no-ts` | - | TypeScript for web apps |
|
||||||
|
|
||||||
|
## Deployment Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) |
|
||||||
|
| `--git` / `--no-git` | Initialize git (default: yes) |
|
||||||
|
| `--open` | Open browser after deploy |
|
||||||
|
|
||||||
|
## Advanced Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--template=user/repo` | GitHub template or local path |
|
||||||
|
| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) |
|
||||||
|
| `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) |
|
||||||
|
| `--experimental` | Enable experimental features |
|
||||||
|
| `--wrangler-defaults` | Skip wrangler prompts |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLOUDFLARE_API_TOKEN=xxx # For deployment
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=xxx # Account ID
|
||||||
|
CF_TELEMETRY_DISABLED=1 # Disable telemetry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
`0` success, `1` user abort, `2` error
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
For framework apps, follow [Framework Setup](README.md#framework-setup).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TypeScript Worker
|
||||||
|
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy
|
||||||
|
|
||||||
|
# Astro blog
|
||||||
|
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy
|
||||||
|
|
||||||
|
# CI: non-interactive
|
||||||
|
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-git --no-deploy
|
||||||
|
|
||||||
|
# GitHub template
|
||||||
|
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
|
||||||
|
|
||||||
|
# Convert existing project
|
||||||
|
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js
|
||||||
|
```
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# C3 Generated Configuration
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
my-app/
|
||||||
|
├── src/index.ts # Worker entry point
|
||||||
|
├── wrangler.jsonc # Cloudflare config
|
||||||
|
├── package.json # Scripts
|
||||||
|
├── tsconfig.json
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## wrangler.jsonc
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
|
||||||
|
"name": "my-app",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2026-01-27"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Binding Placeholders
|
||||||
|
|
||||||
|
C3 generates **placeholder IDs** that must be replaced before deploy:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }],
|
||||||
|
"d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Replace with real IDs:**
|
||||||
|
```bash
|
||||||
|
npx wrangler kv namespace create MY_KV # Returns real ID
|
||||||
|
npx wrangler d1 create my-database # Returns real database_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deployment error if not replaced:**
|
||||||
|
```
|
||||||
|
Error: Invalid KV namespace ID "placeholder_kv_id"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"cf-typegen": "wrangler types"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Generation
|
||||||
|
|
||||||
|
Run after adding bindings:
|
||||||
|
```bash
|
||||||
|
npm run cf-typegen
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates `.wrangler/types/runtime.d.ts`:
|
||||||
|
```typescript
|
||||||
|
interface Env {
|
||||||
|
MY_KV: KVNamespace;
|
||||||
|
DB: D1Database;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Creation Checklist
|
||||||
|
|
||||||
|
1. Review `wrangler.jsonc` - check name, compatibility_date
|
||||||
|
2. Replace placeholder binding IDs with real resource IDs
|
||||||
|
3. Run `npm run cf-typegen`
|
||||||
|
4. Test: `npm run dev`
|
||||||
|
5. Deploy: `npm run deploy`
|
||||||
|
6. Add secrets: `npx wrangler secret put SECRET_NAME`
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# C3 Troubleshooting
|
||||||
|
|
||||||
|
## Deployment Issues
|
||||||
|
|
||||||
|
### Placeholder IDs
|
||||||
|
|
||||||
|
**Error:** "Invalid namespace ID"
|
||||||
|
**Fix:** Replace placeholders in wrangler.jsonc with real IDs:
|
||||||
|
```bash
|
||||||
|
npx wrangler kv namespace create MY_KV # Get real ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
**Error:** "Not authenticated"
|
||||||
|
**Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN`
|
||||||
|
|
||||||
|
### Name Conflict
|
||||||
|
|
||||||
|
**Error:** "Worker already exists"
|
||||||
|
**Fix:** Change `name` in wrangler.jsonc
|
||||||
|
|
||||||
|
## Platform Selection
|
||||||
|
|
||||||
|
| Need | Platform |
|
||||||
|
|------|----------|
|
||||||
|
| Git integration, branch previews | `--platform=pages` |
|
||||||
|
| Durable Objects, D1, Queues | Workers (default) |
|
||||||
|
|
||||||
|
Wrong platform? Recreate with correct `--platform` flag.
|
||||||
|
|
||||||
|
## TypeScript Issues
|
||||||
|
|
||||||
|
**"Cannot find name 'KVNamespace'"**
|
||||||
|
```bash
|
||||||
|
npm run cf-typegen # Regenerate types
|
||||||
|
# Restart TS server in editor
|
||||||
|
```
|
||||||
|
|
||||||
|
**Missing types after config change:** Re-run `npm run cf-typegen`
|
||||||
|
|
||||||
|
## Package Manager
|
||||||
|
|
||||||
|
**Multiple lockfiles causing issues:**
|
||||||
|
```bash
|
||||||
|
rm pnpm-lock.yaml # If using npm
|
||||||
|
rm package-lock.json # If using pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
**CI hangs on prompts:**
|
||||||
|
```bash
|
||||||
|
npm create cloudflare@latest my-app -- \
|
||||||
|
--type=hello-world --lang=ts --no-git --no-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auth in CI:**
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Framework-Specific
|
||||||
|
|
||||||
|
| Framework | Issue | Fix |
|
||||||
|
|-----------|-------|-----|
|
||||||
|
| Next.js | create-next-app failed | `npm cache clean --force`, retry |
|
||||||
|
| Astro | Adapter missing | Install `@astrojs/cloudflare` |
|
||||||
|
| Remix | Module errors | Update `@remix-run/cloudflare*` |
|
||||||
|
|
||||||
|
## Compatibility Date
|
||||||
|
|
||||||
|
**"Feature X requires compatibility_date >= ..."**
|
||||||
|
**Fix:** Update `compatibility_date` in wrangler.jsonc to today's date
|
||||||
|
|
||||||
|
## Node.js Version
|
||||||
|
|
||||||
|
**"Node.js version not supported"**
|
||||||
|
**Fix:** Install Node.js 18+ (`nvm install 20`)
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| Invalid namespace ID | Placeholder binding | Create resource, update config |
|
||||||
|
| Not authenticated | No login | `npx wrangler login` |
|
||||||
|
| Cannot find KVNamespace | Missing types | `npm run cf-typegen` |
|
||||||
|
| Worker already exists | Name conflict | Change `name` |
|
||||||
|
| CI hangs | Missing flags | Add --type, --lang, --no-deploy |
|
||||||
|
| Template not found | Bad name | Check cloudflare/templates |
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# C3 Usage Patterns
|
||||||
|
|
||||||
|
## Quick Workflows
|
||||||
|
|
||||||
|
For framework apps, follow [Framework Setup](README.md#framework-setup).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TypeScript API Worker
|
||||||
|
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy
|
||||||
|
|
||||||
|
# Astro static site
|
||||||
|
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD (GitHub Actions)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Deploy
|
||||||
|
run: npm run deploy
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Non-interactive requires:**
|
||||||
|
```bash
|
||||||
|
--type=<value> # Required
|
||||||
|
--no-git # Recommended (CI already in git)
|
||||||
|
--no-deploy # Deploy separately with secrets
|
||||||
|
--framework=<value> # For web-app
|
||||||
|
--ts / --no-ts # Required
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monorepo
|
||||||
|
|
||||||
|
C3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/
|
||||||
|
npm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Templates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GitHub repo
|
||||||
|
npm create cloudflare@latest -- --template=username/repo
|
||||||
|
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
|
||||||
|
|
||||||
|
# Local path
|
||||||
|
npm create cloudflare@latest my-app -- --template=../my-template
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template requires `c3.config.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-template",
|
||||||
|
"category": "hello-world",
|
||||||
|
"copies": [{ "path": "src/" }, { "path": "wrangler.jsonc" }],
|
||||||
|
"transforms": [{ "path": "package.json", "jsonc": { "name": "{{projectName}}" }}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Existing Projects
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add Cloudflare to existing Worker
|
||||||
|
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
For existing framework apps, follow [Framework Setup](README.md#framework-setup).
|
||||||
|
|
||||||
|
## Post-Creation Checklist
|
||||||
|
|
||||||
|
1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name`
|
||||||
|
2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create`
|
||||||
|
3. Generate types: `npm run cf-typegen`
|
||||||
|
4. Test: `npm run dev`
|
||||||
|
5. Deploy: `npm run deploy`
|
||||||
|
6. Set secrets: `wrangler secret put SECRET_NAME`
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Cloudflare Cache Reserve
|
||||||
|
|
||||||
|
**Persistent cache storage built on R2 for long-term content retention**
|
||||||
|
|
||||||
|
## Smart Shield Integration
|
||||||
|
|
||||||
|
Cache Reserve is part of **Smart Shield**, Cloudflare's comprehensive security and performance suite:
|
||||||
|
|
||||||
|
- **Smart Shield Advanced tier**: Includes 2TB Cache Reserve storage
|
||||||
|
- **Standalone purchase**: Available separately if not using Smart Shield
|
||||||
|
- **Migration**: Existing standalone customers can migrate to Smart Shield bundles
|
||||||
|
|
||||||
|
**Decision**: Already on Smart Shield Advanced? Cache Reserve is included. Otherwise evaluate standalone purchase vs Smart Shield upgrade.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Cache Reserve is Cloudflare's persistent, large-scale cache storage layer built on R2. It acts as the ultimate upper-tier cache, storing cacheable content for extended periods (30+ days) to maximize cache hits, reduce origin egress fees, and shield origins from repeated requests for long-tail content.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### What is Cache Reserve?
|
||||||
|
|
||||||
|
- **Persistent storage layer**: Built on R2, sits above tiered cache hierarchy
|
||||||
|
- **Long-term retention**: 30-day default retention, extended on each access
|
||||||
|
- **Automatic operation**: Works seamlessly with existing CDN, no code changes required
|
||||||
|
- **Origin shielding**: Dramatically reduces origin egress by serving cached content longer
|
||||||
|
- **Usage-based pricing**: Pay only for storage + read/write operations
|
||||||
|
|
||||||
|
### Cache Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
Visitor Request
|
||||||
|
↓
|
||||||
|
Lower-Tier Cache (closest to visitor)
|
||||||
|
↓ (on miss)
|
||||||
|
Upper-Tier Cache (closest to origin)
|
||||||
|
↓ (on miss)
|
||||||
|
Cache Reserve (R2 persistent storage)
|
||||||
|
↓ (on miss)
|
||||||
|
Origin Server
|
||||||
|
```
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **On cache miss**: Content fetched from origin �� written to Cache Reserve + edge caches simultaneously
|
||||||
|
2. **On edge eviction**: Content may be evicted from edge cache but remains in Cache Reserve
|
||||||
|
3. **On subsequent request**: If edge cache misses but Cache Reserve hits → content restored to edge caches
|
||||||
|
4. **Retention**: Assets remain in Cache Reserve for 30 days since last access (configurable via TTL)
|
||||||
|
|
||||||
|
## When to Use Cache Reserve
|
||||||
|
|
||||||
|
```
|
||||||
|
Need persistent caching?
|
||||||
|
├─ High origin egress costs → Cache Reserve ✓
|
||||||
|
├─ Long-tail content (archives, media libraries) → Cache Reserve ✓
|
||||||
|
├─ Already using Smart Shield Advanced → Included! ✓
|
||||||
|
├─ Video streaming with seeking (range requests) → ✗ Not supported
|
||||||
|
├─ Dynamic/personalized content → ✗ Use edge cache only
|
||||||
|
├─ Need per-request cache control from Workers → ✗ Use R2 directly
|
||||||
|
└─ Frequently updated content (< 10hr lifetime) → ✗ Not eligible
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asset Eligibility
|
||||||
|
|
||||||
|
Cache Reserve only stores assets meeting **ALL** criteria:
|
||||||
|
|
||||||
|
- Cacheable per Cloudflare's standard rules
|
||||||
|
- Minimum 10-hour TTL (36000 seconds)
|
||||||
|
- `Content-Length` header present
|
||||||
|
- Original files only (not transformed images)
|
||||||
|
|
||||||
|
### Eligibility Checklist
|
||||||
|
|
||||||
|
Use this checklist to verify if an asset is eligible:
|
||||||
|
|
||||||
|
- [ ] Zone has Cache Reserve enabled
|
||||||
|
- [ ] Zone has Tiered Cache enabled (required)
|
||||||
|
- [ ] Asset TTL ≥ 10 hours (36,000 seconds)
|
||||||
|
- [ ] `Content-Length` header present on origin response
|
||||||
|
- [ ] No `Set-Cookie` header (or uses private directive)
|
||||||
|
- [ ] `Vary` header is NOT `*` (can be `Accept-Encoding`)
|
||||||
|
- [ ] Not an image transformation variant (original images OK)
|
||||||
|
- [ ] Not a range request (no HTTP 206 support)
|
||||||
|
- [ ] Not O2O (Orange-to-Orange) proxied request
|
||||||
|
|
||||||
|
**All boxes must be checked for Cache Reserve eligibility.**
|
||||||
|
|
||||||
|
### Not Eligible
|
||||||
|
|
||||||
|
- Assets with TTL < 10 hours
|
||||||
|
- Responses without `Content-Length` header
|
||||||
|
- Image transformation variants (original images are eligible)
|
||||||
|
- Responses with `Set-Cookie` headers
|
||||||
|
- Responses with `Vary: *` header
|
||||||
|
- Assets from R2 public buckets on same zone
|
||||||
|
- O2O (Orange-to-Orange) setup requests
|
||||||
|
- **Range requests** (video seeking, partial content downloads)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable via Dashboard
|
||||||
|
https://dash.cloudflare.com/caching/cache-reserve
|
||||||
|
# Click "Enable Storage Sync" or "Purchase" button
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- Paid Cache Reserve plan or Smart Shield Advanced required
|
||||||
|
- Tiered Cache required for optimal performance
|
||||||
|
|
||||||
|
## Essential Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Cache Reserve status
|
||||||
|
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN"
|
||||||
|
|
||||||
|
# Enable Cache Reserve
|
||||||
|
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"value": "on"}'
|
||||||
|
|
||||||
|
# Check asset cache status
|
||||||
|
curl -I https://example.com/asset.jpg | grep -i cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
| Task | Files |
|
||||||
|
|------|-------|
|
||||||
|
| Evaluate if Cache Reserve fits your use case | README.md (this file) |
|
||||||
|
| Enable Cache Reserve for your zone | README.md + [configuration.md](./configuration.md) |
|
||||||
|
| Use with Workers (understand limitations) | [api.md](./api.md) |
|
||||||
|
| Setup via SDKs or IaC (TypeScript, Python, Terraform) | [configuration.md](./configuration.md) |
|
||||||
|
| Optimize costs and debug issues | [patterns.md](./patterns.md) + [gotchas.md](./gotchas.md) |
|
||||||
|
| Understand eligibility and troubleshoot | [gotchas.md](./gotchas.md) → [patterns.md](./patterns.md) |
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- [configuration.md](./configuration.md) - Setup, API, SDKs, and Cache Rules
|
||||||
|
- [api.md](./api.md) - Purging, monitoring, Workers integration
|
||||||
|
- [patterns.md](./patterns.md) - Best practices, cost optimization, debugging
|
||||||
|
- [gotchas.md](./gotchas.md) - Common issues, limitations, troubleshooting
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
- [r2](../r2/) - Cache Reserve built on R2 storage
|
||||||
|
- [workers](https://developers.cloudflare.com/workers/) - Workers integration with Cache API
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
# Cache Reserve API
|
||||||
|
|
||||||
|
## Workers Integration
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CRITICAL: Workers Cache API ≠ Cache Reserve │
|
||||||
|
│ │
|
||||||
|
│ • Workers caches.default / cache.put() → edge cache ONLY │
|
||||||
|
│ • Cache Reserve → zone-level setting, automatic, no per-req │
|
||||||
|
│ • You CANNOT selectively write to Cache Reserve from Workers │
|
||||||
|
│ • Cache Reserve works with standard fetch(), not cache.put() │
|
||||||
|
└────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Cache Reserve is a **zone-level configuration**, not a per-request API. It works automatically when enabled for the zone:
|
||||||
|
|
||||||
|
### Standard Fetch (Recommended)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Cache Reserve works automatically via standard fetch
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
// Standard fetch uses Cache Reserve automatically
|
||||||
|
return await fetch(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache API Limitations
|
||||||
|
|
||||||
|
**IMPORTANT**: `cache.put()` is **NOT compatible** with Cache Reserve or Tiered Cache.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ WRONG: cache.put() bypasses Cache Reserve
|
||||||
|
const cache = caches.default;
|
||||||
|
let response = await cache.match(request);
|
||||||
|
if (!response) {
|
||||||
|
response = await fetch(request);
|
||||||
|
await cache.put(request, response.clone()); // Bypasses Cache Reserve!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ CORRECT: Use standard fetch for Cache Reserve compatibility
|
||||||
|
return await fetch(request);
|
||||||
|
|
||||||
|
// ✅ CORRECT: Use Cache API only for custom cache namespaces
|
||||||
|
const customCache = await caches.open('my-custom-cache');
|
||||||
|
let response = await customCache.match(request);
|
||||||
|
if (!response) {
|
||||||
|
response = await fetch(request);
|
||||||
|
await customCache.put(request, response.clone()); // Custom cache OK
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Purging and Cache Management
|
||||||
|
|
||||||
|
### Purge by URL (Instant)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Purge specific URL from Cache Reserve immediately
|
||||||
|
const purgeCacheReserveByURL = async (
|
||||||
|
zoneId: string,
|
||||||
|
apiToken: string,
|
||||||
|
urls: string[]
|
||||||
|
) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ files: urls })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
await purgeCacheReserveByURL('zone123', 'token456', [
|
||||||
|
'https://example.com/image.jpg',
|
||||||
|
'https://example.com/video.mp4'
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Purge by Tag/Host/Prefix (Revalidation)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Purge by cache tag - forces revalidation, not immediate removal
|
||||||
|
await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tags: ['tag1', 'tag2'] })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purge behavior:**
|
||||||
|
- **By URL**: Immediate removal from Cache Reserve + edge cache
|
||||||
|
- **By tag/host/prefix**: Revalidation only, assets remain in storage (costs continue)
|
||||||
|
|
||||||
|
### Clear All Cache Reserve Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Requires Cache Reserve OFF first
|
||||||
|
await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/zones/${zoneId}/cache/cache_reserve_clear`,
|
||||||
|
{ method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check status: GET same endpoint returns { state: "In-progress" | "Completed" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Process**: Disable Cache Reserve → Call clear endpoint → Wait up to 24hr → Re-enable
|
||||||
|
|
||||||
|
## Monitoring and Analytics
|
||||||
|
|
||||||
|
### Dashboard Analytics
|
||||||
|
|
||||||
|
Navigate to **Caching > Cache Reserve** to view:
|
||||||
|
|
||||||
|
- **Egress Savings**: Total bytes served from Cache Reserve vs origin egress cost saved
|
||||||
|
- **Requests Served**: Cache Reserve hits vs misses breakdown
|
||||||
|
- **Storage Used**: Current GB stored in Cache Reserve (billed monthly)
|
||||||
|
- **Operations**: Class A (writes) and Class B (reads) operation counts
|
||||||
|
- **Cost Tracking**: Estimated monthly costs based on current usage
|
||||||
|
|
||||||
|
### Logpush Integration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Logpush field: CacheReserveUsed (boolean) - filter for Cache Reserve hits
|
||||||
|
// Query Cache Reserve hits in analytics
|
||||||
|
const logpushQuery = `
|
||||||
|
SELECT
|
||||||
|
ClientRequestHost,
|
||||||
|
COUNT(*) as requests,
|
||||||
|
SUM(EdgeResponseBytes) as bytes_served,
|
||||||
|
COUNT(CASE WHEN CacheReserveUsed = true THEN 1 END) as cache_reserve_hits,
|
||||||
|
COUNT(CASE WHEN CacheReserveUsed = false THEN 1 END) as cache_reserve_misses
|
||||||
|
FROM http_requests
|
||||||
|
WHERE Timestamp >= NOW() - INTERVAL '24 hours'
|
||||||
|
GROUP BY ClientRequestHost
|
||||||
|
ORDER BY requests DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Filter only Cache Reserve hits
|
||||||
|
const crHitsQuery = `
|
||||||
|
SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes
|
||||||
|
FROM http_requests
|
||||||
|
WHERE CacheReserveUsed = true AND Timestamp >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY ClientRequestHost
|
||||||
|
ORDER BY bytes DESC
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### GraphQL Analytics
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query CacheReserveAnalytics($zoneTag: string, $since: string, $until: string) {
|
||||||
|
viewer {
|
||||||
|
zones(filter: { zoneTag: $zoneTag }) {
|
||||||
|
httpRequests1dGroups(
|
||||||
|
filter: { datetime_geq: $since, datetime_leq: $until }
|
||||||
|
limit: 1000
|
||||||
|
) {
|
||||||
|
dimensions { date }
|
||||||
|
sum {
|
||||||
|
cachedBytes
|
||||||
|
cachedRequests
|
||||||
|
bytes
|
||||||
|
requests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pricing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Storage: $0.015/GB-month | Class A (writes): $4.50/M | Class B (reads): $0.36/M
|
||||||
|
// Cache miss: 1A + 1B | Cache hit: 1B | Assets >1GB: proportionally more ops
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [README](./README.md) - Overview and core concepts
|
||||||
|
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||||
|
- [Patterns](./patterns.md) - Best practices and optimization
|
||||||
|
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Cache Reserve Configuration
|
||||||
|
|
||||||
|
## Dashboard Setup
|
||||||
|
|
||||||
|
**Minimum steps to enable:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to dashboard
|
||||||
|
https://dash.cloudflare.com/caching/cache-reserve
|
||||||
|
|
||||||
|
# Click "Enable Storage Sync" or "Purchase" button
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:**
|
||||||
|
- Paid Cache Reserve plan or Smart Shield Advanced required
|
||||||
|
- Tiered Cache **required** for Cache Reserve to function optimally
|
||||||
|
|
||||||
|
## API Configuration
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable
|
||||||
|
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"value": "on"}'
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||||
|
-H "Authorization: Bearer $API_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript SDK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from 'cloudflare';
|
||||||
|
|
||||||
|
const client = new Cloudflare({
|
||||||
|
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable Cache Reserve
|
||||||
|
await client.cache.cacheReserve.edit({
|
||||||
|
zone_id: 'abc123',
|
||||||
|
value: 'on',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Cache Reserve status
|
||||||
|
const status = await client.cache.cacheReserve.get({
|
||||||
|
zone_id: 'abc123',
|
||||||
|
});
|
||||||
|
console.log(status.value); // 'on' or 'off'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python SDK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from cloudflare import Cloudflare
|
||||||
|
|
||||||
|
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
|
||||||
|
|
||||||
|
# Enable Cache Reserve
|
||||||
|
client.cache.cache_reserve.edit(
|
||||||
|
zone_id="abc123",
|
||||||
|
value="on"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get Cache Reserve status
|
||||||
|
status = client.cache.cache_reserve.get(zone_id="abc123")
|
||||||
|
print(status.value) # 'on' or 'off'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terraform
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
cloudflare = {
|
||||||
|
source = "cloudflare/cloudflare"
|
||||||
|
version = "~> 4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "cloudflare" {
|
||||||
|
api_token = var.cloudflare_api_token
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "cloudflare_zone_cache_reserve" "example" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tiered Cache is required for Cache Reserve
|
||||||
|
resource "cloudflare_tiered_cache" "example" {
|
||||||
|
zone_id = var.zone_id
|
||||||
|
cache_type = "smart"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pulumi
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as cloudflare from "@pulumi/cloudflare";
|
||||||
|
|
||||||
|
// Enable Cache Reserve
|
||||||
|
const cacheReserve = new cloudflare.ZoneCacheReserve("example", {
|
||||||
|
zoneId: zoneId,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable Tiered Cache (required)
|
||||||
|
const tieredCache = new cloudflare.TieredCache("example", {
|
||||||
|
zoneId: zoneId,
|
||||||
|
cacheType: "smart",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required API Token Permissions
|
||||||
|
|
||||||
|
- `Zone Settings Read`
|
||||||
|
- `Zone Settings Write`
|
||||||
|
- `Zone Read`
|
||||||
|
- `Zone Write`
|
||||||
|
|
||||||
|
## Cache Rules Integration
|
||||||
|
|
||||||
|
Control Cache Reserve eligibility via Cache Rules:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enable for static assets
|
||||||
|
{
|
||||||
|
action: 'set_cache_settings',
|
||||||
|
action_parameters: {
|
||||||
|
cache_reserve: { eligible: true, minimum_file_ttl: 86400 },
|
||||||
|
edge_ttl: { mode: 'override_origin', default: 86400 },
|
||||||
|
cache: true
|
||||||
|
},
|
||||||
|
expression: '(http.request.uri.path matches "\\.(jpg|png|webp|pdf|zip)$")'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable for APIs
|
||||||
|
{
|
||||||
|
action: 'set_cache_settings',
|
||||||
|
action_parameters: { cache_reserve: { eligible: false } },
|
||||||
|
expression: '(http.request.uri.path matches "^/api/")'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create via API: PUT to zones/{zone_id}/rulesets/phases/http_request_cache_settings/entrypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrangler Integration
|
||||||
|
|
||||||
|
Cache Reserve works automatically with Workers deployed via Wrangler. No special wrangler.jsonc configuration needed - enable Cache Reserve via Dashboard or API for the zone.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [README](./README.md) - Overview and core concepts
|
||||||
|
- [API Reference](./api.md) - Purging and monitoring APIs
|
||||||
|
- [Patterns](./patterns.md) - Best practices and optimization
|
||||||
|
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Cache Reserve Gotchas
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### "Assets Not Being Cached in Cache Reserve"
|
||||||
|
|
||||||
|
**Cause:** Asset is not cacheable, TTL < 10 hours, Content-Length header missing, or blocking headers present (Set-Cookie, Vary: *)
|
||||||
|
**Solution:** Ensure minimum TTL of 10+ hours (`Cache-Control: public, max-age=36000`), add Content-Length header, remove Set-Cookie header, and set `Vary: Accept-Encoding` (not *)
|
||||||
|
|
||||||
|
### "Range Requests Not Working" (Video Seeking Fails)
|
||||||
|
|
||||||
|
**Cause:** Cache Reserve does **NOT** support range requests (HTTP 206 Partial Content)
|
||||||
|
**Solution:** Range requests bypass Cache Reserve entirely. For video streaming with seeking:
|
||||||
|
- Use edge cache only (shorter TTLs)
|
||||||
|
- Consider R2 with direct access for range-heavy workloads
|
||||||
|
- Accept that seekable content won't benefit from Cache Reserve persistence
|
||||||
|
|
||||||
|
### "Origin Bandwidth Higher Than Expected"
|
||||||
|
|
||||||
|
**Cause:** Cache Reserve fetches **uncompressed** content from origin, even though it serves compressed to visitors
|
||||||
|
**Solution:**
|
||||||
|
- If origin charges by bandwidth, factor in uncompressed transfer costs
|
||||||
|
- Cache Reserve compresses for visitors automatically (saves visitor bandwidth)
|
||||||
|
- Compare: origin egress savings vs higher uncompressed fetch costs
|
||||||
|
|
||||||
|
### "Cloudflare Images Not Caching with Cache Reserve"
|
||||||
|
|
||||||
|
**Cause:** Cloudflare Images with `Vary: Accept` header (format negotiation) is incompatible with Cache Reserve
|
||||||
|
**Solution:**
|
||||||
|
- Cache Reserve silently skips images with Vary for format negotiation
|
||||||
|
- Original images (non-transformed) may still be eligible
|
||||||
|
- Use Cloudflare Images variants or edge cache for transformed images
|
||||||
|
|
||||||
|
### "High Class A Operations Costs"
|
||||||
|
|
||||||
|
**Cause:** Frequent cache misses, short TTLs, or frequent revalidation
|
||||||
|
**Solution:** Increase TTL for stable content (24+ hours), enable Tiered Cache to reduce direct Cache Reserve misses, or use stale-while-revalidate
|
||||||
|
|
||||||
|
### "Purge Not Working as Expected"
|
||||||
|
|
||||||
|
**Cause:** Purge by tag only triggers revalidation but doesn't remove from Cache Reserve storage
|
||||||
|
**Solution:** Use purge by URL for immediate removal, or disable Cache Reserve then clear all data for complete removal
|
||||||
|
|
||||||
|
### "O2O (Orange-to-Orange) Assets Not Caching"
|
||||||
|
|
||||||
|
**Cause:** Orange-to-Orange (proxied zone requesting another proxied zone on Cloudflare) bypasses Cache Reserve
|
||||||
|
**Solution:**
|
||||||
|
- **What is O2O**: Zone A (proxied) → Zone B (proxied), both on Cloudflare
|
||||||
|
- **Detection**: Check `cf-cache-status` for `BYPASS` and review request path
|
||||||
|
- **Workaround**: Use R2 or direct origin access instead of O2O proxy chains
|
||||||
|
|
||||||
|
### "Cache Reserve must be OFF before clearing data"
|
||||||
|
|
||||||
|
**Cause:** Attempting to clear Cache Reserve data while it's still enabled
|
||||||
|
**Solution:** Disable Cache Reserve first, wait briefly for propagation (5s), then clear data (can take up to 24 hours)
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Limit | Value | Notes |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| Minimum TTL | 10 hours (36000 seconds) | Assets with shorter TTL not eligible |
|
||||||
|
| Default retention | 30 days (2592000 seconds) | Configurable |
|
||||||
|
| Maximum file size | Same as R2 limits | No practical limit |
|
||||||
|
| Purge/clear time | Up to 24 hours | Complete propagation time |
|
||||||
|
| Plan requirement | Paid Cache Reserve or Smart Shield | Not available on free plans |
|
||||||
|
| Content-Length header | Required | Must be present for eligibility |
|
||||||
|
| Set-Cookie header | Blocks caching | Must not be present (or use private directive) |
|
||||||
|
| Vary header | Cannot be * | Can use Vary: Accept-Encoding |
|
||||||
|
| Image transformations | Variants not eligible | Original images only |
|
||||||
|
| Range requests | NOT supported | HTTP 206 bypasses Cache Reserve |
|
||||||
|
| Compression | Fetches uncompressed | Serves compressed to visitors |
|
||||||
|
| Worker control | Zone-level only | Cannot control per-request |
|
||||||
|
| O2O requests | Bypassed | Orange-to-Orange not eligible |
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- **Official Docs**: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/
|
||||||
|
- **API Reference**: https://developers.cloudflare.com/api/resources/cache/subresources/cache_reserve/
|
||||||
|
- **Cache Rules**: https://developers.cloudflare.com/cache/how-to/cache-rules/
|
||||||
|
- **Workers Cache API**: https://developers.cloudflare.com/workers/runtime-apis/cache/
|
||||||
|
- **R2 Documentation**: https://developers.cloudflare.com/r2/
|
||||||
|
- **Smart Shield**: https://developers.cloudflare.com/smart-shield/
|
||||||
|
- **Tiered Cache**: https://developers.cloudflare.com/cache/how-to/tiered-cache/
|
||||||
|
|
||||||
|
## Troubleshooting Flowchart
|
||||||
|
|
||||||
|
Asset not caching in Cache Reserve?
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Is Cache Reserve enabled for zone?
|
||||||
|
→ No: Enable via Dashboard or API
|
||||||
|
→ Yes: Continue to step 2
|
||||||
|
|
||||||
|
2. Is Tiered Cache enabled?
|
||||||
|
→ No: Enable Tiered Cache (required!)
|
||||||
|
→ Yes: Continue to step 3
|
||||||
|
|
||||||
|
3. Does asset have TTL ≥ 10 hours?
|
||||||
|
→ No: Increase via Cache Rules (edge_ttl override)
|
||||||
|
→ Yes: Continue to step 4
|
||||||
|
|
||||||
|
4. Is Content-Length header present?
|
||||||
|
→ No: Fix origin to include Content-Length
|
||||||
|
→ Yes: Continue to step 5
|
||||||
|
|
||||||
|
5. Is Set-Cookie header present?
|
||||||
|
→ Yes: Remove Set-Cookie or scope appropriately
|
||||||
|
→ No: Continue to step 6
|
||||||
|
|
||||||
|
6. Is Vary header set to *?
|
||||||
|
→ Yes: Change to specific value (e.g., Accept-Encoding)
|
||||||
|
→ No: Continue to step 7
|
||||||
|
|
||||||
|
7. Is this a range request?
|
||||||
|
→ Yes: Range requests bypass Cache Reserve (not supported)
|
||||||
|
→ No: Continue to step 8
|
||||||
|
|
||||||
|
8. Is this an O2O (Orange-to-Orange) request?
|
||||||
|
→ Yes: O2O bypasses Cache Reserve
|
||||||
|
→ No: Continue to step 9
|
||||||
|
|
||||||
|
9. Check Logpush CacheReserveUsed field
|
||||||
|
→ Filter logs to see if assets ever hit Cache Reserve
|
||||||
|
→ Verify cf-cache-status header (should be HIT after first request)
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [README](./README.md) - Overview and core concepts
|
||||||
|
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||||
|
- [API Reference](./api.md) - Purging and monitoring
|
||||||
|
- [Patterns](./patterns.md) - Best practices and optimization
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Cache Reserve Patterns
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Always Enable Tiered Cache
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Cache Reserve is designed for use WITH Tiered Cache
|
||||||
|
const configuration = {
|
||||||
|
tieredCache: 'enabled', // Required for optimal performance
|
||||||
|
cacheReserve: 'enabled', // Works best with Tiered Cache
|
||||||
|
|
||||||
|
hierarchy: [
|
||||||
|
'Lower-Tier Cache (visitor)',
|
||||||
|
'Upper-Tier Cache (origin region)',
|
||||||
|
'Cache Reserve (persistent)',
|
||||||
|
'Origin'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Appropriate Cache-Control Headers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Origin response headers for Cache Reserve eligibility
|
||||||
|
const originHeaders = {
|
||||||
|
'Cache-Control': 'public, max-age=86400', // 24hr (minimum 10hr)
|
||||||
|
'Content-Length': '1024000', // Required
|
||||||
|
'Cache-Tag': 'images,product-123', // Optional: purging
|
||||||
|
'ETag': '"abc123"', // Optional: revalidation
|
||||||
|
// Avoid: 'Set-Cookie' and 'Vary: *' prevent caching
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Cache Rules for Fine-Grained Control
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Different TTLs for different content types
|
||||||
|
const cacheRules = [
|
||||||
|
{
|
||||||
|
description: 'Long-term cache for immutable assets',
|
||||||
|
expression: '(http.request.uri.path matches "^/static/.*\\.[a-f0-9]{8}\\.")',
|
||||||
|
action_parameters: {
|
||||||
|
cache_reserve: { eligible: true },
|
||||||
|
edge_ttl: { mode: 'override_origin', default: 2592000 }, // 30 days
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Moderate cache for regular images',
|
||||||
|
expression: '(http.request.uri.path matches "\\.(jpg|png|webp)$")',
|
||||||
|
action_parameters: {
|
||||||
|
cache_reserve: { eligible: true },
|
||||||
|
edge_ttl: { mode: 'override_origin', default: 86400 }, // 24 hours
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Exclude API from Cache Reserve',
|
||||||
|
expression: '(http.request.uri.path matches "^/api/")',
|
||||||
|
action_parameters: { cache_reserve: { eligible: false }, cache: false }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Making Assets Cache Reserve Eligible from Workers
|
||||||
|
|
||||||
|
**Note**: This modifies response headers to meet eligibility criteria but does NOT directly control Cache Reserve storage (which is zone-level automatic).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const response = await fetch(request);
|
||||||
|
if (!response.ok) return response;
|
||||||
|
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set('Cache-Control', 'public, max-age=36000'); // 10hr minimum
|
||||||
|
headers.delete('Set-Cookie'); // Blocks caching
|
||||||
|
|
||||||
|
// Ensure Content-Length present
|
||||||
|
if (!headers.has('Content-Length')) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
headers.set('Content-Length', blob.size.toString());
|
||||||
|
return new Response(blob, { status: response.status, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(response.body, { status: response.status, headers });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Hostname Best Practices
|
||||||
|
|
||||||
|
Use Worker's hostname for efficient caching - avoid overriding hostname unnecessarily.
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Multi-Tier Caching + Immutable Assets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Optimal: L1 (visitor) → L2 (region) → L3 (Cache Reserve) → Origin
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const isImmutable = /\.[a-f0-9]{8,}\.(js|css|jpg|png|woff2)$/.test(url.pathname);
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
if (isImmutable) {
|
||||||
|
const headers = new Headers(response.headers);
|
||||||
|
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
return new Response(response.body, { status: response.status, headers });
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cost Optimization
|
||||||
|
|
||||||
|
### Cost Calculator
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CacheReserveEstimate {
|
||||||
|
avgAssetSizeGB: number;
|
||||||
|
uniqueAssets: number;
|
||||||
|
monthlyReads: number;
|
||||||
|
monthlyWrites: number;
|
||||||
|
originEgressCostPerGB: number; // e.g., AWS: $0.09/GB
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateMonthlyCost(input: CacheReserveEstimate) {
|
||||||
|
// Cache Reserve pricing
|
||||||
|
const storageCostPerGBMonth = 0.015;
|
||||||
|
const classAPerMillion = 4.50; // writes
|
||||||
|
const classBPerMillion = 0.36; // reads
|
||||||
|
|
||||||
|
// Calculate Cache Reserve costs
|
||||||
|
const totalStorageGB = input.avgAssetSizeGB * input.uniqueAssets;
|
||||||
|
const storageCost = totalStorageGB * storageCostPerGBMonth;
|
||||||
|
const writeCost = (input.monthlyWrites / 1_000_000) * classAPerMillion;
|
||||||
|
const readCost = (input.monthlyReads / 1_000_000) * classBPerMillion;
|
||||||
|
|
||||||
|
const cacheReserveCost = storageCost + writeCost + readCost;
|
||||||
|
|
||||||
|
// Calculate origin egress cost (what you'd pay without Cache Reserve)
|
||||||
|
const totalTrafficGB = (input.monthlyReads * input.avgAssetSizeGB);
|
||||||
|
const originEgressCost = totalTrafficGB * input.originEgressCostPerGB;
|
||||||
|
|
||||||
|
// Savings calculation
|
||||||
|
const savings = originEgressCost - cacheReserveCost;
|
||||||
|
const savingsPercent = ((savings / originEgressCost) * 100).toFixed(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cacheReserveCost: `$${cacheReserveCost.toFixed(2)}`,
|
||||||
|
originEgressCost: `$${originEgressCost.toFixed(2)}`,
|
||||||
|
monthlySavings: `$${savings.toFixed(2)}`,
|
||||||
|
savingsPercent: `${savingsPercent}%`,
|
||||||
|
breakdown: {
|
||||||
|
storage: `$${storageCost.toFixed(2)}`,
|
||||||
|
writes: `$${writeCost.toFixed(2)}`,
|
||||||
|
reads: `$${readCost.toFixed(2)}`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Media library
|
||||||
|
const mediaLibrary = estimateMonthlyCost({
|
||||||
|
avgAssetSizeGB: 0.005, // 5MB images
|
||||||
|
uniqueAssets: 10_000,
|
||||||
|
monthlyReads: 5_000_000,
|
||||||
|
monthlyWrites: 50_000,
|
||||||
|
originEgressCostPerGB: 0.09, // AWS S3
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(mediaLibrary);
|
||||||
|
// {
|
||||||
|
// cacheReserveCost: "$9.98",
|
||||||
|
// originEgressCost: "$25.00",
|
||||||
|
// monthlySavings: "$15.02",
|
||||||
|
// savingsPercent: "60.1%",
|
||||||
|
// breakdown: { storage: "$0.75", writes: "$0.23", reads: "$9.00" }
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimization Guidelines
|
||||||
|
|
||||||
|
- **Set appropriate TTLs**: 10hr minimum, 24hr+ optimal for stable content, 30d max cautiously
|
||||||
|
- **Cache high-value stable assets**: Images, media, fonts, archives, documentation
|
||||||
|
- **Exclude frequently changing**: APIs, user-specific content, real-time data
|
||||||
|
- **Compression note**: Cache Reserve fetches uncompressed from origin, serves compressed to visitors - factor in origin egress costs
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [README](./README.md) - Overview and core concepts
|
||||||
|
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||||
|
- [API Reference](./api.md) - Purging and monitoring
|
||||||
|
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Cloudflare Containers
|
||||||
|
|
||||||
|
Use this reference for containerized applications on the Workers platform, including container-enabled Durable Objects, lifecycle management, and request routing.
|
||||||
|
|
||||||
|
## Choose the runtime
|
||||||
|
|
||||||
|
Use [Containers](https://developers.cloudflare.com/containers/) for existing container images, custom runtimes, system dependencies, full filesystem access, or workloads needing additional CPU and memory. Use [Workers](https://developers.cloudflare.com/workers/) when the application fits the Workers runtime without those requirements.
|
||||||
|
|
||||||
|
Containers are controlled through [Durable Objects](https://developers.cloudflare.com/durable-objects/). An instance's identity does not make its filesystem persistent: design for restarts and store durable data outside the container disk. Read [Container lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/) and [Container interface](https://developers.cloudflare.com/containers/reference/container-class/) for the relationship between the process, its Durable Object, and persistent storage.
|
||||||
|
|
||||||
|
## Find the documentation for the task
|
||||||
|
|
||||||
|
Read the linked page before writing code or configuration; use its current API, examples, and constraints rather than reconstructing them from memory.
|
||||||
|
|
||||||
|
| Task | Start here |
|
||||||
|
| --- | --- |
|
||||||
|
| Create a project and deploy the first container | [Get started](https://developers.cloudflare.com/containers/get-started/) |
|
||||||
|
| Configure images, bindings, instance sizes, and deployments | [Configuration](configuration.md) |
|
||||||
|
| Control startup, requests, lifecycle, and scheduling | [API](api.md) |
|
||||||
|
| Choose routing or connect other services | [Patterns](patterns.md) |
|
||||||
|
| Diagnose startup, persistence, capacity, or rollout issues | [Gotchas](gotchas.md) |
|
||||||
|
|
||||||
|
For additional topics, consult the [Containers documentation index](https://developers.cloudflare.com/containers/llms.txt).
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Containers API
|
||||||
|
|
||||||
|
Use the [Container interface](https://developers.cloudflare.com/containers/reference/container-class/) for the current SDK methods, signatures, properties, and examples. Use the [Durable Object Container API](https://developers.cloudflare.com/durable-objects/api/container/) when working directly with the runtime rather than the SDK class.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Forward HTTP or WebSocket requests | [Request methods](https://developers.cloudflare.com/containers/reference/container-class/#request-methods) |
|
||||||
|
| Start a process, wait for ports, stop, or destroy it | [Start and stop](https://developers.cloudflare.com/containers/reference/container-class/#start-and-stop) |
|
||||||
|
| React to startup, exit, errors, or idle expiry | [Lifecycle hooks](https://developers.cloudflare.com/containers/reference/container-class/#lifecycle-hooks) |
|
||||||
|
| Inspect state or keep background work active | [State and monitoring](https://developers.cloudflare.com/containers/reference/container-class/#state-and-monitoring) |
|
||||||
|
| Schedule callbacks without replacing the SDK's alarm handler | [Scheduling](https://developers.cloudflare.com/containers/reference/container-class/#scheduling) |
|
||||||
|
| Address named instances, select stateless instances, or switch request ports | [Utility functions](https://developers.cloudflare.com/containers/reference/container-class/#utility-functions) |
|
||||||
|
| Communicate over TCP from a Durable Object | [TCP port API](https://developers.cloudflare.com/durable-objects/api/container/#gettcpport) |
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Containers configuration
|
||||||
|
|
||||||
|
Read the relevant documentation before choosing configuration fields or resource sizes.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Configure the container image, Durable Object binding, class, migrations, and instance count | [Wrangler Containers configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#containers) |
|
||||||
|
| Select a predefined size or configure custom CPU, memory, and disk | [Limits and instance types](https://developers.cloudflare.com/containers/platform/limits/) and [custom instance configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#custom-instance-types) |
|
||||||
|
| Set ports, readiness checks, idle timeout, entrypoint, or internet access | [Container properties](https://developers.cloudflare.com/containers/reference/container-class/#properties) |
|
||||||
|
| Set runtime variables or pass secrets per instance | [Environment variables](https://developers.cloudflare.com/containers/configuration/environment-variables/) and [environment variables and secrets example](https://developers.cloudflare.com/containers/examples/env-vars-and-secrets/) |
|
||||||
|
| Build images or use existing registry images | [Image management](https://developers.cloudflare.com/containers/guides/image-management/) |
|
||||||
|
| Run and iterate locally | [Local development](https://developers.cloudflare.com/containers/guides/local-dev/) |
|
||||||
|
| Deploy from a workstation or Workers Builds | [Deploy Containers](https://developers.cloudflare.com/containers/guides/deploy/) |
|
||||||
|
| Control image updates and replacement of running instances | [Rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/) |
|
||||||
|
| Estimate resource and network costs | [Pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Containers troubleshooting
|
||||||
|
|
||||||
|
Use the current documentation to diagnose behavior instead of relying on copied timeout values, resource limits, or lifecycle recipes.
|
||||||
|
|
||||||
|
| Symptom or concern | Documentation to read |
|
||||||
|
| --- | --- |
|
||||||
|
| Startup timeout or unavailable port | [Start and stop](https://developers.cloudflare.com/containers/reference/container-class/#start-and-stop), [Container properties](https://developers.cloudflare.com/containers/reference/container-class/#properties), and [first-deploy provisioning](https://developers.cloudflare.com/containers/get-started/) |
|
||||||
|
| WebSocket forwarding fails | [WebSocket example](https://developers.cloudflare.com/containers/examples/websocket/) and [request methods](https://developers.cloudflare.com/containers/reference/container-class/#request-methods) |
|
||||||
|
| Background work stops on idle expiry | [Activity renewal](https://developers.cloudflare.com/containers/reference/container-class/#renewactivitytimeout) and [idle expiry hook](https://developers.cloudflare.com/containers/reference/container-class/#onactivityexpired) |
|
||||||
|
| Scheduled callbacks do not run | [Scheduling and alarm ownership](https://developers.cloudflare.com/containers/reference/container-class/#scheduling) |
|
||||||
|
| Shutdown cleanup or filesystem data loss | [Container shutdown and disk lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/#container-shutdown) |
|
||||||
|
| Out-of-memory errors or resource exhaustion | [FAQ](https://developers.cloudflare.com/containers/faq/) and [limits and instance types](https://developers.cloudflare.com/containers/platform/limits/) |
|
||||||
|
| Instance count exceeded or unexpected request distribution | [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#containers) and [scaling and routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/) |
|
||||||
|
| Worker and container image versions differ after deployment | [Deployment behavior](https://developers.cloudflare.com/containers/guides/deploy/) and [rollouts](https://developers.cloudflare.com/containers/configuration/rollouts/) |
|
||||||
|
| Local behavior differs from deployed behavior | [Local development](https://developers.cloudflare.com/containers/guides/local-dev/) |
|
||||||
|
| Logs, cold starts, or runtime availability questions | [FAQ](https://developers.cloudflare.com/containers/faq/) |
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Containers patterns
|
||||||
|
|
||||||
|
Choose instance identity based on the workload: per-user/session or per-job identities for affinity, one shared identity for a singleton, and interchangeable instances for stateless requests. Read [scaling and routing](https://developers.cloudflare.com/containers/configuration/scaling-and-routing/) for current helpers and scaling behavior before implementing that choice.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Distribute requests across stateless instances | [Stateless instances example](https://developers.cloudflare.com/containers/examples/stateless/) |
|
||||||
|
| Forward WebSocket connections | [WebSocket example](https://developers.cloudflare.com/containers/examples/websocket/) |
|
||||||
|
| React to lifecycle changes | [Status hooks example](https://developers.cloudflare.com/containers/examples/status-hooks/) |
|
||||||
|
| Handle shutdown and persist data across restarts | [Container lifecycle](https://developers.cloudflare.com/containers/concepts/architecture/) and [Container interface](https://developers.cloudflare.com/containers/reference/container-class/) |
|
||||||
|
| Keep long operations active or schedule callbacks | [Activity renewal](https://developers.cloudflare.com/containers/reference/container-class/#renewactivitytimeout) and [scheduling](https://developers.cloudflare.com/containers/reference/container-class/#scheduling) |
|
||||||
|
| Start containers on a cron schedule | [Cron container example](https://developers.cloudflare.com/containers/examples/cron/) |
|
||||||
|
| Route requests to multiple ports | [Request methods](https://developers.cloudflare.com/containers/reference/container-class/#request-methods) and [utility functions](https://developers.cloudflare.com/containers/reference/container-class/#utility-functions) |
|
||||||
|
| Access Workers bindings from the container | [Connect to Workers and bindings](https://developers.cloudflare.com/containers/configuration/workers-connections/) |
|
||||||
|
|
||||||
|
## Workflows and Queues
|
||||||
|
|
||||||
|
For multi-step orchestration, combine the [Workflows Workers API](https://developers.cloudflare.com/workflows/build/workers-api/) with the [Container API](api.md). For queue-driven jobs, read the [Queues consumer API](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) and [acknowledgement and retry behavior](https://developers.cloudflare.com/queues/configuration/batching-retries/#explicit-acknowledgement-and-retries) alongside the Container API. These pages document the component APIs; they are not end-to-end Container integration examples.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Cloudflare Cron Triggers
|
||||||
|
|
||||||
|
Use Cron Triggers to start periodic Worker jobs. Fetch the relevant current documentation before implementing; configuration, API signatures, examples, and limits belong in the docs.
|
||||||
|
|
||||||
|
- **Set up a recurring job:** [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) covers scheduling, deployment, and execution history.
|
||||||
|
- **Implement the job:** [Scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) covers controller properties, asynchronous work, and multiple schedules.
|
||||||
|
- **Schedule durable work:** [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/) covers direct Workflow schedules and starting instances from a Worker. Check this before introducing a Worker whose only job is to start a Workflow.
|
||||||
|
- **Check capacity:** fetch [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) for the target plan and invocation type.
|
||||||
|
|
||||||
|
## In This Reference
|
||||||
|
|
||||||
|
- [configuration.md](./configuration.md) — schedule setup, environments, removal, and Green Compute
|
||||||
|
- [api.md](./api.md) — handler implementation, asynchronous completion, and tests
|
||||||
|
- [patterns.md](./patterns.md) — choosing execution boundaries and integrations
|
||||||
|
- [gotchas.md](./gotchas.md) — investigating timing, failures, and repeated work
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Workflows](../workflows/README.md) — durable multi-step jobs
|
||||||
|
- [Queues](../queues/README.md) — asynchronous message processing
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cron Triggers API
|
||||||
|
|
||||||
|
Fetch the handler documentation before writing code; use its current language examples and completion semantics.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Implement the handler and access the cron expression, scheduled time, bindings, and context | [Scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) |
|
||||||
|
| Route different schedules to different operations | [Handle multiple cron triggers](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#handle-multiple-cron-triggers) |
|
||||||
|
| Await work and understand how asynchronous failures affect invocation status | [Handler methods](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#methods) |
|
||||||
|
| Invoke a scheduled handler locally with a chosen expression or time | [Test Cron Triggers locally](https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers-locally) |
|
||||||
|
| Build tests using runtime-backed controllers and execution contexts | [Workers test APIs](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/) |
|
||||||
|
| Start and inspect a Workflow instance | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/) |
|
||||||
|
|
||||||
|
Decide which operation establishes successful completion and make its failures observable. Test each configured schedule and partial-failure recovery. Read the local-testing documentation for the current endpoint and query parameters instead of adding a production HTTP route to imitate the development helper.
|
||||||
|
|
||||||
|
See [patterns.md](./patterns.md) for execution design and [gotchas.md](./gotchas.md) for failures.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cron Triggers Configuration
|
||||||
|
|
||||||
|
Fetch the documentation for the configuration operation before changing schedules.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Add a handler and configure triggers, including per-environment schedules and deployment propagation | [Add a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/#add-a-cron-trigger) |
|
||||||
|
| Choose an expression, interpret weekday numbering, or check supported extensions | [Supported cron expressions](https://developers.cloudflare.com/workers/configuration/cron-triggers/#supported-cron-expressions) |
|
||||||
|
| Remove schedules or distinguish omission from an empty configuration | [Remove a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/#remove-a-cron-trigger) |
|
||||||
|
| Configure renewable-energy execution locations | [Green Compute](https://developers.cloudflare.com/workers/configuration/cron-triggers/#green-compute) |
|
||||||
|
| Check trigger counts and execution budgets | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) |
|
||||||
|
| Schedule Workflow instances directly | [Schedule a Workflow directly](https://developers.cloudflare.com/workflows/build/trigger-workflows/#schedule-a-workflow-directly) |
|
||||||
|
|
||||||
|
Identify the target environment and the intended business timezone before choosing an expression. Review which schedules a deployment will replace, and use the documented propagation behavior when planning a rollout. For Green Compute, follow its account-level configuration rather than inferring settings from Worker placement.
|
||||||
|
|
||||||
|
See [api.md](./api.md) for implementation and [gotchas.md](./gotchas.md) for verification.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Cron Triggers Troubleshooting
|
||||||
|
|
||||||
|
Investigate using the current documentation rather than copied limits or assumed delivery guarantees.
|
||||||
|
|
||||||
|
| Symptom or question | Documentation and check |
|
||||||
|
| --- | --- |
|
||||||
|
| Job runs at an unexpected time | Check [UTC execution](https://developers.cloudflare.com/workers/configuration/cron-triggers/#background) and [expression syntax](https://developers.cloudflare.com/workers/configuration/cron-triggers/#supported-cron-expressions); compare with the intended business timezone. |
|
||||||
|
| Schedule is missing after deployment | Check the handler, target environment, and propagation guidance in [Add a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/#add-a-cron-trigger). |
|
||||||
|
| Removing or preserving schedules has an unexpected result | Review [Remove a Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/#remove-a-cron-trigger) before changing empty or omitted configuration. |
|
||||||
|
| Local invocation fails | Follow [Test Cron Triggers locally](https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers-locally) for the supported endpoint, port, and query parameters. |
|
||||||
|
| Async work fails or completion status is surprising | Read [handler methods](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#methods) and inspect [past events](https://developers.cloudflare.com/workers/configuration/cron-triggers/#view-past-events). |
|
||||||
|
| Job exceeds its execution budget | Check [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) before choosing a smaller unit of work or [Workflows](../workflows/README.md). |
|
||||||
|
| Green Compute behavior differs from expectations | Read [Green Compute](https://developers.cloudflare.com/workers/configuration/cron-triggers/#green-compute) for its execution-location policy and account configuration. |
|
||||||
|
|
||||||
|
For repeated or partially completed business operations, decide how to identify work and recover safely before selecting storage. Test recovery after each side effect; a marker alone does not establish that the operation completed. Do not assume a particular automatic retry schedule or delivery guarantee without a documented contract.
|
||||||
|
|
||||||
|
See [patterns.md](./patterns.md) for coordination choices and [api.md](./api.md) for tests.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Cron Triggers Patterns
|
||||||
|
|
||||||
|
Choose the execution boundary before writing a scheduled job; fetch the relevant integration docs for implementation.
|
||||||
|
|
||||||
|
| Need | Documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Periodic API sync, cleanup, reports, or health checks in a Worker | [Cron Triggers background](https://developers.cloudflare.com/workers/configuration/cron-triggers/#background) and [scheduled handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) |
|
||||||
|
| Different jobs on different schedules | [Handle multiple cron triggers](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#handle-multiple-cron-triggers) |
|
||||||
|
| Durable multi-step work started on a schedule | [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/) |
|
||||||
|
| Send work to a queue and implement its consumer | [Queues JavaScript APIs](https://developers.cloudflare.com/queues/configuration/javascript-apis/) |
|
||||||
|
| Coordinate state across invocations | [What are Durable Objects?](https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/) |
|
||||||
|
| Inspect whether scheduled work ran | [View past events](https://developers.cloudflare.com/workers/configuration/cron-triggers/#view-past-events) |
|
||||||
|
|
||||||
|
Keep the trigger separate from the business operation so manual recovery and scheduled execution can share it. Decide how partial progress is recorded, how repeated attempts affect side effects, and who owns completion reporting. When distributing a batch, distinguish successful enqueueing from successful processing.
|
||||||
|
|
||||||
|
Use [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) to evaluate whether the job fits one invocation. If it needs durable steps, waiting, or explicit retry boundaries, inspect [Workflows](../workflows/README.md) before building those mechanisms in the handler.
|
||||||
|
|
||||||
|
See [api.md](./api.md) for tests and [gotchas.md](./gotchas.md) for operational checks.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Cloudflare D1 Database
|
||||||
|
|
||||||
|
Use D1 for managed relational application data with SQLite semantics. For an existing external database, consider [Hyperdrive](../hyperdrive/); for per-entity coordination, consider [Durable Objects](https://developers.cloudflare.com/workers/platform/storage-options/#sql-in-durable-objects-vs-d1). See [storage options](https://developers.cloudflare.com/workers/platform/storage-options/) before choosing a product.
|
||||||
|
|
||||||
|
Read the relevant current documentation before implementing. These references route tasks to the source of truth rather than maintaining copies of APIs, configuration, or plan tables.
|
||||||
|
|
||||||
|
## Start here
|
||||||
|
|
||||||
|
- [Get started](https://developers.cloudflare.com/d1/get-started/): create a database, bind it to a Worker, and run a first query.
|
||||||
|
- [configuration.md](./configuration.md): bindings, environments, migrations, local development, and ORM integration.
|
||||||
|
- [api.md](./api.md): parameterized queries, batches, sessions, HTTP access, and testing.
|
||||||
|
- [patterns.md](./patterns.md): query design, caching, tenant isolation, replication, and recovery.
|
||||||
|
- [gotchas.md](./gotchas.md): errors, types, constraints, performance, and limits.
|
||||||
|
|
||||||
|
Check [limits](https://developers.cloudflare.com/d1/platform/limits/) and [pricing](https://developers.cloudflare.com/d1/platform/pricing/) for capacity, allowances, and plan availability; do not infer them from old examples.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# D1 API Reference
|
||||||
|
|
||||||
|
Fetch the relevant API page before writing queries or assuming method signatures and return types.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Bind values and choose a query execution method | [Prepared statement methods](https://developers.cloudflare.com/d1/worker-api/prepared-statements/) |
|
||||||
|
| Execute batches and understand transaction rollback; use database sessions | [D1 Database API](https://developers.cloudflare.com/d1/worker-api/d1-database/) |
|
||||||
|
| Interpret results and query metadata | [Return objects](https://developers.cloudflare.com/d1/worker-api/return-object/) |
|
||||||
|
| Choose supported JavaScript values and TypeScript result types | [Workers Binding API](https://developers.cloudflare.com/d1/worker-api/) |
|
||||||
|
| Choose consistency constraints and carry bookmarks between requests | [Read replication and Sessions API](https://developers.cloudflare.com/d1/best-practices/read-replication/) |
|
||||||
|
| Query from a server-side script outside Workers | [REST query API](https://developers.cloudflare.com/api/resources/d1/subresources/database/methods/query/) |
|
||||||
|
| Handle query failures and transient errors | [Debug D1](https://developers.cloudflare.com/d1/observability/debug-d1/) and [retry queries](https://developers.cloudflare.com/d1/best-practices/retry-queries/) |
|
||||||
|
| Test database queries and apply migrations in tests | [Workers Vitest APIs: D1](https://developers.cloudflare.com/workers/testing/vitest-integration/test-apis/#d1) |
|
||||||
|
|
||||||
|
Bind untrusted values with prepared statements; do not interpolate them into SQL. Parameters do not replace identifiers: choose dynamic table, column, or sort names from an application-controlled allowlist.
|
||||||
|
|
||||||
|
D1 sessions provide sequential consistency for replicated queries. They are not a way to extend query execution limits. Choose the starting constraint or bookmark from the application's consistency requirements using the replication guide.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# D1 Configuration
|
||||||
|
|
||||||
|
Read the task's documentation before adding bindings or running database commands. Confirm the database and environment being targeted, particularly when applying migrations or importing data.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Create a database and attach a Worker binding | [Getting started](https://developers.cloudflare.com/d1/get-started/) |
|
||||||
|
| Configure binding fields and multiple databases | [Wrangler D1 configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases) |
|
||||||
|
| Separate staging and production databases | [D1 environments](https://developers.cloudflare.com/d1/configuration/environments/) |
|
||||||
|
| Create, track, and apply schema migrations | [Migrations](https://developers.cloudflare.com/d1/reference/migrations/) |
|
||||||
|
| Look up CLI flags for management, execution, and exports | [D1 Wrangler commands](https://developers.cloudflare.com/d1/wrangler-commands/) |
|
||||||
|
| Develop against local database state | [Local development](https://developers.cloudflare.com/d1/best-practices/local-development/) |
|
||||||
|
| Generate binding types | [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) |
|
||||||
|
| Choose an ORM or query builder, including Drizzle | [D1 community projects](https://developers.cloudflare.com/d1/reference/community-projects/) (follow the integration's current setup guide) |
|
||||||
|
| Import or export SQL data | [Import and export data](https://developers.cloudflare.com/d1/best-practices/import-export-data/) |
|
||||||
|
| Enable replicas and use them through sessions | [Read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/) |
|
||||||
|
|
||||||
|
Local migrations and data do not automatically update a remote database. Test against a separate staging database before a production migration. Naming another binding `DB_REPLICA` does not configure replica routing; follow the replication guide.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# D1 Gotchas & Troubleshooting
|
||||||
|
|
||||||
|
Use current documentation to diagnose the failure before changing query or database configuration.
|
||||||
|
|
||||||
|
| Symptom or question | What to check |
|
||||||
|
| --- | --- |
|
||||||
|
| Missing table, query exception, or constraint error | [Debug D1](https://developers.cloudflare.com/d1/observability/debug-d1/); verify the target binding, environment, and applied [migrations](https://developers.cloudflare.com/d1/reference/migrations/) |
|
||||||
|
| Boolean, date, or other binding type mismatch | [Workers Binding API type conversion](https://developers.cloudflare.com/d1/worker-api/) and [SQL support](https://developers.cloudflare.com/d1/sql-api/sql-statements/) |
|
||||||
|
| Foreign key failure during writes or migrations | [Foreign key enforcement and deferral](https://developers.cloudflare.com/d1/sql-api/foreign-keys/) |
|
||||||
|
| Slow queries, scans, or excessive rows read | [Indexes and query plans](https://developers.cloudflare.com/d1/best-practices/use-indexes/) and [metrics](https://developers.cloudflare.com/d1/observability/metrics-analytics/) |
|
||||||
|
| Query duration, statement, storage, or account limits | [Current limits](https://developers.cloudflare.com/d1/platform/limits/) |
|
||||||
|
| Unexpected usage charges or plan assumptions | [Pricing](https://developers.cloudflare.com/d1/platform/pricing/) |
|
||||||
|
| Stale reads after a write | [Sessions, bookmarks, and read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/) |
|
||||||
|
| Transient query failures | [Retry guidance](https://developers.cloudflare.com/d1/best-practices/retry-queries/); check idempotency before retrying writes |
|
||||||
|
| Import/export failure or unsupported data | [Import/export behavior and limitations](https://developers.cloudflare.com/d1/best-practices/import-export-data/) |
|
||||||
|
| Local and deployed databases differ | [Local development](https://developers.cloudflare.com/d1/best-practices/local-development/) and [environment configuration](https://developers.cloudflare.com/d1/configuration/environments/) |
|
||||||
|
|
||||||
|
Continue to bind untrusted SQL values as described in [api.md](./api.md). Do not treat SQL injection as a recoverable database error or assume retries correct invalid queries.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# D1 Patterns & Best Practices
|
||||||
|
|
||||||
|
Use these guides to design the operation, then fetch [api.md](./api.md) for implementation references.
|
||||||
|
|
||||||
|
| Task | Current documentation |
|
||||||
|
| --- | --- |
|
||||||
|
| Design pagination, filters, joins, and aggregations | [Query a database](https://developers.cloudflare.com/d1/best-practices/query-d1/) and [supported SQL](https://developers.cloudflare.com/d1/sql-api/sql-statements/) |
|
||||||
|
| Reduce scans and inspect query plans | [Use indexes](https://developers.cloudflare.com/d1/best-practices/use-indexes/) |
|
||||||
|
| Batch writes or transform data | [Database API](https://developers.cloudflare.com/d1/worker-api/d1-database/) and [limits](https://developers.cloudflare.com/d1/platform/limits/) |
|
||||||
|
| Store and query event metadata | [Query JSON](https://developers.cloudflare.com/d1/sql-api/query-json/) |
|
||||||
|
| Evaluate a cache in front of D1 | [How KV works](https://developers.cloudflare.com/kv/concepts/how-kv-works/) |
|
||||||
|
| Choose shared or per-tenant databases | [D1 FAQs](https://developers.cloudflare.com/d1/reference/faq/) and [limits](https://developers.cloudflare.com/d1/platform/limits/) |
|
||||||
|
| Reduce read latency while preserving required consistency | [Read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/) |
|
||||||
|
| Plan point-in-time recovery or portable backups | [Time Travel](https://developers.cloudflare.com/d1/reference/time-travel/) and [import/export](https://developers.cloudflare.com/d1/best-practices/import-export-data/) |
|
||||||
|
|
||||||
|
Keep result sets bounded and pagination ordering deterministic. Choose indexes from actual query plans. When splitting a large operation into batches, account for the loss of whole-operation atomicity across batches.
|
||||||
|
|
||||||
|
Authorize a tenant before selecting its database or rows; a request header alone is not proof of tenant membership. Application login sessions stored in tables are separate from D1's Sessions API.
|
||||||
|
|
||||||
|
Before caching reads, decide how stale data may be and how writes invalidate cached results. For replicated reads, choose session constraints and bookmark propagation based on read-after-write requirements rather than assuming every read sees the latest primary state.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Cloudflare DDoS Protection
|
||||||
|
|
||||||
|
Autonomous, always-on protection against DDoS attacks across L3/4 and L7.
|
||||||
|
|
||||||
|
## Protection Types
|
||||||
|
|
||||||
|
- **HTTP DDoS (L7)**: Protects HTTP/HTTPS traffic, phase `ddos_l7`, zone/account level
|
||||||
|
- **Network DDoS (L3/4)**: UDP/SYN/DNS floods, phase `ddos_l4`, account level only
|
||||||
|
- **Adaptive DDoS**: Learns 7-day baseline, detects deviations, 4 profile types (Origins, User-Agents, Locations, Protocols)
|
||||||
|
|
||||||
|
## Plan Availability
|
||||||
|
|
||||||
|
| Feature | Free | Pro | Business | Enterprise | Enterprise Advanced |
|
||||||
|
|---------|------|-----|----------|------------|---------------------|
|
||||||
|
| HTTP DDoS (L7) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Network DDoS (L3/4) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Override rules | 1 | 1 | 1 | 1 | 10 |
|
||||||
|
| Custom expressions | ✗ | ✗ | ✗ | ✗ | ✓ |
|
||||||
|
| Log action | ✗ | ✗ | ✗ | ✗ | ✓ |
|
||||||
|
| Adaptive DDoS | ✗ | ✗ | ✗ | ✓ | ✓ |
|
||||||
|
| Alert filters | Basic | Basic | Basic | Advanced | Advanced |
|
||||||
|
|
||||||
|
## Actions & Sensitivity
|
||||||
|
|
||||||
|
- **Actions**: `block`, `managed_challenge`, `challenge`, `log` (Enterprise Advanced only)
|
||||||
|
- **Sensitivity**: `default` (high), `medium`, `low`, `eoff` (essentially off)
|
||||||
|
- **Override**: By category/tag or individual rule ID
|
||||||
|
- **Scope**: Zone-level overrides take precedence over account-level
|
||||||
|
|
||||||
|
## Reading Order
|
||||||
|
|
||||||
|
| File | Purpose | Start Here If... |
|
||||||
|
|------|---------|------------------|
|
||||||
|
| [configuration.md](./configuration.md) | Dashboard setup, rule structure, adaptive profiles | You're setting up DDoS protection for the first time |
|
||||||
|
| [api.md](./api.md) | API endpoints, SDK usage, ruleset ID discovery | You're automating configuration or need programmatic access |
|
||||||
|
| [patterns.md](./patterns.md) | Protection strategies, defense-in-depth, dynamic response | You need implementation patterns or layered security |
|
||||||
|
| [gotchas.md](./gotchas.md) | False positives, tuning, error handling | You're troubleshooting or optimizing existing protection |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
- [waf](../waf/) - Application-layer security rules
|
||||||
|
- [bot-management](../bot-management/) - Bot detection and mitigation
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# DDoS API
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### HTTP DDoS (L7)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Zone-level
|
||||||
|
PUT /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint
|
||||||
|
GET /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint
|
||||||
|
|
||||||
|
// Account-level (Enterprise Advanced)
|
||||||
|
PUT /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint
|
||||||
|
GET /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network DDoS (L3/4)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Account-level only
|
||||||
|
PUT /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint
|
||||||
|
GET /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript SDK
|
||||||
|
|
||||||
|
**SDK Version**: Requires `cloudflare` >= 3.0.0 for ruleset phase methods.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Cloudflare from "cloudflare";
|
||||||
|
|
||||||
|
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
|
||||||
|
|
||||||
|
// STEP 1: Discover managed ruleset ID (required for overrides)
|
||||||
|
const allRulesets = await client.rulesets.list({ zone_id: zoneId });
|
||||||
|
const ddosRuleset = allRulesets.result.find(
|
||||||
|
(r) => r.kind === "managed" && r.phase === "ddos_l7"
|
||||||
|
);
|
||||||
|
if (!ddosRuleset) throw new Error("DDoS managed ruleset not found");
|
||||||
|
const managedRulesetId = ddosRuleset.id;
|
||||||
|
|
||||||
|
// STEP 2: Get current HTTP DDoS configuration
|
||||||
|
const entrypointRuleset = await client.zones.rulesets.phases.entrypoint.get("ddos_l7", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// STEP 3: Update HTTP DDoS ruleset with overrides
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
action: "execute",
|
||||||
|
expression: "true",
|
||||||
|
action_parameters: {
|
||||||
|
id: managedRulesetId, // From discovery step
|
||||||
|
overrides: {
|
||||||
|
sensitivity_level: "medium",
|
||||||
|
action: "managed_challenge",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network DDoS (account level, L3/4)
|
||||||
|
const l4Rulesets = await client.rulesets.list({ account_id: accountId });
|
||||||
|
const l4DdosRuleset = l4Rulesets.result.find(
|
||||||
|
(r) => r.kind === "managed" && r.phase === "ddos_l4"
|
||||||
|
);
|
||||||
|
const l4Ruleset = await client.accounts.rulesets.phases.entrypoint.get("ddos_l4", {
|
||||||
|
account_id: accountId,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alert Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DDoSAlertConfig {
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
alert_type: "http_ddos_attack_alert" | "layer_3_4_ddos_attack_alert"
|
||||||
|
| "advanced_http_ddos_attack_alert" | "advanced_layer_3_4_ddos_attack_alert";
|
||||||
|
filters?: {
|
||||||
|
zones?: string[];
|
||||||
|
hostnames?: string[];
|
||||||
|
requests_per_second?: number;
|
||||||
|
packets_per_second?: number;
|
||||||
|
megabits_per_second?: number;
|
||||||
|
ip_prefixes?: string[]; // CIDR
|
||||||
|
ip_addresses?: string[];
|
||||||
|
protocols?: string[];
|
||||||
|
};
|
||||||
|
mechanisms: {
|
||||||
|
email?: Array<{ id: string }>;
|
||||||
|
webhooks?: Array<{ id: string }>;
|
||||||
|
pagerduty?: Array<{ id: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create alert
|
||||||
|
await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${accountId}/alerting/v3/policies`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(alertConfig),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typed Override Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Override by category
|
||||||
|
interface CategoryOverride {
|
||||||
|
action: "execute";
|
||||||
|
expression: string;
|
||||||
|
action_parameters: {
|
||||||
|
id: string;
|
||||||
|
overrides: {
|
||||||
|
categories?: Array<{
|
||||||
|
category: "http-flood" | "http-anomaly" | "udp-flood" | "syn-flood";
|
||||||
|
sensitivity_level?: "default" | "medium" | "low" | "eoff";
|
||||||
|
action?: "block" | "managed_challenge" | "challenge" | "log";
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override by rule ID
|
||||||
|
interface RuleOverride {
|
||||||
|
action: "execute";
|
||||||
|
expression: string;
|
||||||
|
action_parameters: {
|
||||||
|
id: string;
|
||||||
|
overrides: {
|
||||||
|
rules?: Array<{
|
||||||
|
id: string;
|
||||||
|
action?: "block" | "managed_challenge" | "challenge" | "log";
|
||||||
|
sensitivity_level?: "default" | "medium" | "low" | "eoff";
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Override specific adaptive rule
|
||||||
|
const adaptiveOverride: RuleOverride = {
|
||||||
|
action: "execute",
|
||||||
|
expression: "true",
|
||||||
|
action_parameters: {
|
||||||
|
id: managedRulesetId,
|
||||||
|
overrides: {
|
||||||
|
rules: [
|
||||||
|
{ id: "...adaptive-origins-rule-id...", sensitivity_level: "low" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
See [patterns.md](./patterns.md) for complete implementation patterns.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# DDoS Configuration
|
||||||
|
|
||||||
|
## Dashboard Setup
|
||||||
|
|
||||||
|
1. Navigate to Security > DDoS
|
||||||
|
2. Select HTTP DDoS or Network-layer DDoS
|
||||||
|
3. Configure sensitivity & action per ruleset/category/rule
|
||||||
|
4. Apply overrides with optional expressions (Enterprise Advanced)
|
||||||
|
5. Enable Adaptive DDoS toggle (Enterprise/Enterprise Advanced, requires 7 days traffic history)
|
||||||
|
|
||||||
|
## Rule Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DDoSOverride {
|
||||||
|
description: string;
|
||||||
|
rules: Array<{
|
||||||
|
action: "execute";
|
||||||
|
expression: string; // Custom expression (Enterprise Advanced) or "true" for all
|
||||||
|
action_parameters: {
|
||||||
|
id: string; // Managed ruleset ID (discover via api.md)
|
||||||
|
overrides: {
|
||||||
|
sensitivity_level?: "default" | "medium" | "low" | "eoff";
|
||||||
|
action?: "block" | "managed_challenge" | "challenge" | "log"; // log = Enterprise Advanced only
|
||||||
|
categories?: Array<{
|
||||||
|
category: string; // e.g., "http-flood", "udp-flood"
|
||||||
|
sensitivity_level?: string;
|
||||||
|
}>;
|
||||||
|
rules?: Array<{
|
||||||
|
id: string;
|
||||||
|
action?: string;
|
||||||
|
sensitivity_level?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expression Availability
|
||||||
|
|
||||||
|
| Plan | Custom Expressions | Example |
|
||||||
|
|------|-------------------|---------|
|
||||||
|
| Free/Pro/Business | ✗ | Use `"true"` only |
|
||||||
|
| Enterprise | ✗ | Use `"true"` only |
|
||||||
|
| Enterprise Advanced | ✓ | `ip.src in {...}`, `http.request.uri.path matches "..."` |
|
||||||
|
|
||||||
|
## Sensitivity Mapping
|
||||||
|
|
||||||
|
| UI | API | Threshold |
|
||||||
|
|----|-----|-----------|
|
||||||
|
| High | `default` | Most aggressive |
|
||||||
|
| Medium | `medium` | Balanced |
|
||||||
|
| Low | `low` | Less aggressive |
|
||||||
|
| Essentially Off | `eoff` | Minimal mitigation |
|
||||||
|
|
||||||
|
## Common Categories
|
||||||
|
|
||||||
|
- `http-flood`, `http-anomaly` (L7)
|
||||||
|
- `udp-flood`, `syn-flood`, `dns-flood` (L3/4)
|
||||||
|
|
||||||
|
## Override Precedence
|
||||||
|
|
||||||
|
Multiple override layers apply in this order (higher precedence wins):
|
||||||
|
|
||||||
|
```
|
||||||
|
Zone-level > Account-level
|
||||||
|
Individual Rule > Category > Global sensitivity/action
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**: Zone rule for `/api/*` overrides account-level global settings.
|
||||||
|
|
||||||
|
## Adaptive DDoS Profiles
|
||||||
|
|
||||||
|
**Availability**: Enterprise, Enterprise Advanced
|
||||||
|
**Learning period**: 7 days of traffic history required
|
||||||
|
|
||||||
|
| Profile Type | Description | Detects |
|
||||||
|
|--------------|-------------|---------|
|
||||||
|
| **Origins** | Traffic patterns per origin server | Anomalous requests to specific origins |
|
||||||
|
| **User-Agents** | Traffic patterns per User-Agent | Malicious/anomalous user agent strings |
|
||||||
|
| **Locations** | Traffic patterns per geo-location | Attacks from specific countries/regions |
|
||||||
|
| **Protocols** | Traffic patterns per protocol (L3/4) | Protocol-specific flood attacks |
|
||||||
|
|
||||||
|
Configure by targeting specific adaptive rule IDs via API (see api.md#typed-override-examples).
|
||||||
|
|
||||||
|
## Alerting
|
||||||
|
|
||||||
|
Configure via Notifications:
|
||||||
|
- Alert types: `http_ddos_attack_alert`, `layer_3_4_ddos_attack_alert`, `advanced_*` variants
|
||||||
|
- Filters: zones, hostnames, RPS/PPS/Mbps thresholds, IPs, protocols
|
||||||
|
- Mechanisms: email, webhooks, PagerDuty
|
||||||
|
|
||||||
|
See [api.md](./api.md#alert-configuration) for API examples.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# DDoS Gotchas
|
||||||
|
|
||||||
|
## Common Errors
|
||||||
|
|
||||||
|
### "False positives blocking legitimate traffic"
|
||||||
|
|
||||||
|
**Cause**: Sensitivity too high, wrong action, or missing exceptions
|
||||||
|
**Solution**:
|
||||||
|
1. Lower sensitivity for specific rule/category
|
||||||
|
2. Use `log` action first to validate (Enterprise Advanced)
|
||||||
|
3. Add exception with custom expression (e.g., allowlist IPs)
|
||||||
|
4. Query flagged requests via GraphQL Analytics API to identify patterns
|
||||||
|
|
||||||
|
### "Attacks getting through"
|
||||||
|
|
||||||
|
**Cause**: Sensitivity too low or wrong action
|
||||||
|
**Solution**: Increase to `default` sensitivity and use `block` action:
|
||||||
|
```typescript
|
||||||
|
const config = {
|
||||||
|
rules: [{
|
||||||
|
expression: "true",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } },
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Adaptive rules not working"
|
||||||
|
|
||||||
|
**Cause**: Insufficient traffic history (needs 7 days)
|
||||||
|
**Solution**: Wait for baseline to establish, check dashboard for adaptive rule status
|
||||||
|
|
||||||
|
### "Zone override ignored"
|
||||||
|
|
||||||
|
**Cause**: Account overrides conflict with zone overrides
|
||||||
|
**Solution**: Configure at zone level OR remove zone overrides to use account-level
|
||||||
|
|
||||||
|
### "Log action not available"
|
||||||
|
|
||||||
|
**Cause**: Not on Enterprise Advanced DDoS plan
|
||||||
|
**Solution**: Use `managed_challenge` with low sensitivity for testing
|
||||||
|
|
||||||
|
### "Rule limit exceeded"
|
||||||
|
|
||||||
|
**Cause**: Too many override rules (Free/Pro/Business: 1, Enterprise Advanced: 10)
|
||||||
|
**Solution**: Combine conditions in single expression using `and`/`or`
|
||||||
|
|
||||||
|
### "Cannot override rule"
|
||||||
|
|
||||||
|
**Cause**: Rule is read-only
|
||||||
|
**Solution**: Check API response for read-only indicator, use different rule
|
||||||
|
|
||||||
|
### "Cannot disable DDoS protection"
|
||||||
|
|
||||||
|
**Cause**: DDoS managed rulesets cannot be fully disabled (always-on protection)
|
||||||
|
**Solution**: Set `sensitivity_level: "eoff"` for minimal mitigation
|
||||||
|
|
||||||
|
### "Expression not allowed"
|
||||||
|
|
||||||
|
**Cause**: Custom expressions require Enterprise Advanced plan
|
||||||
|
**Solution**: Use `expression: "true"` for all traffic, or upgrade plan
|
||||||
|
|
||||||
|
### "Managed ruleset not found"
|
||||||
|
|
||||||
|
**Cause**: Zone/account doesn't have DDoS managed ruleset, or incorrect phase
|
||||||
|
**Solution**: Verify ruleset exists via `client.rulesets.list()`, check phase name (`ddos_l7` or `ddos_l4`)
|
||||||
|
|
||||||
|
## API Error Codes
|
||||||
|
|
||||||
|
| Error Code | Message | Cause | Solution |
|
||||||
|
|------------|---------|-------|----------|
|
||||||
|
| 10000 | Authentication error | Invalid/missing API token | Check token has DDoS permissions |
|
||||||
|
| 81000 | Ruleset validation failed | Invalid rule structure | Verify `action_parameters.id` is managed ruleset ID |
|
||||||
|
| 81020 | Expression not allowed | Custom expressions on wrong plan | Use `"true"` or upgrade to Enterprise Advanced |
|
||||||
|
| 81021 | Rule limit exceeded | Too many override rules | Reduce rules or upgrade (Enterprise Advanced: 10) |
|
||||||
|
| 81022 | Invalid sensitivity level | Wrong sensitivity value | Use: `default`, `medium`, `low`, `eoff` |
|
||||||
|
| 81023 | Invalid action | Wrong action for plan | Enterprise Advanced only: `log` action |
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Resource/Limit | Free/Pro/Business | Enterprise | Enterprise Advanced |
|
||||||
|
|----------------|-------------------|------------|---------------------|
|
||||||
|
| Override rules per zone | 1 | 1 | 10 |
|
||||||
|
| Custom expressions | ✗ | ✗ | ✓ |
|
||||||
|
| Log action | ✗ | ✗ | ✓ |
|
||||||
|
| Adaptive DDoS | ✗ | ✓ | ✓ |
|
||||||
|
| Traffic history required | - | 7 days | 7 days |
|
||||||
|
|
||||||
|
## Tuning Strategy
|
||||||
|
|
||||||
|
1. Start with `log` action + `medium` sensitivity
|
||||||
|
2. Monitor for 24-48 hours
|
||||||
|
3. Identify false positives, add exceptions
|
||||||
|
4. Gradually increase to `default` sensitivity
|
||||||
|
5. Change action from `log` → `managed_challenge` → `block`
|
||||||
|
6. Document all adjustments
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Test during low-traffic periods
|
||||||
|
- Use zone-level for per-site tuning
|
||||||
|
- Reference IP lists for easier management
|
||||||
|
- Set appropriate alert thresholds (avoid noise)
|
||||||
|
- Combine with WAF for layered defense
|
||||||
|
- Avoid over-tuning (keep config simple)
|
||||||
|
|
||||||
|
See [patterns.md](./patterns.md) for progressive rollout examples.
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# DDoS Protection Patterns
|
||||||
|
|
||||||
|
## Allowlist Trusted IPs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = {
|
||||||
|
description: "Allowlist trusted IPs",
|
||||||
|
rules: [{
|
||||||
|
expression: "ip.src in { 203.0.113.0/24 192.0.2.1 }",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: {
|
||||||
|
id: managedRulesetId,
|
||||||
|
overrides: { sensitivity_level: "eoff" },
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.accounts.rulesets.phases.entrypoint.update("ddos_l7", {
|
||||||
|
account_id: accountId,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route-specific Sensitivity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = {
|
||||||
|
description: "Route-specific protection",
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
expression: "not http.request.uri.path matches \"^/api/\"",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: {
|
||||||
|
id: managedRulesetId,
|
||||||
|
overrides: { sensitivity_level: "default", action: "block" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expression: "http.request.uri.path matches \"^/api/\"",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: {
|
||||||
|
id: managedRulesetId,
|
||||||
|
overrides: { sensitivity_level: "low", action: "managed_challenge" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progressive Enhancement
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ProtectionLevel { MONITORING = "monitoring", LOW = "low", MEDIUM = "medium", HIGH = "high" }
|
||||||
|
|
||||||
|
const levelConfig = {
|
||||||
|
[ProtectionLevel.MONITORING]: { action: "log", sensitivity: "eoff" },
|
||||||
|
[ProtectionLevel.LOW]: { action: "managed_challenge", sensitivity: "low" },
|
||||||
|
[ProtectionLevel.MEDIUM]: { action: "managed_challenge", sensitivity: "medium" },
|
||||||
|
[ProtectionLevel.HIGH]: { action: "block", sensitivity: "default" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
async function setProtectionLevel(zoneId: string, level: ProtectionLevel, rulesetId: string, client: Cloudflare) {
|
||||||
|
const settings = levelConfig[level];
|
||||||
|
return client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [{
|
||||||
|
expression: "true",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: { id: rulesetId, overrides: { action: settings.action, sensitivity_level: settings.sensitivity } },
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Response to Attacks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Env { CLOUDFLARE_API_TOKEN: string; ZONE_ID: string; KV: KVNamespace; }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
if (request.url.includes("/attack-detected")) {
|
||||||
|
const attackData = await request.json();
|
||||||
|
await env.KV.put(`attack:${Date.now()}`, JSON.stringify(attackData), { expirationTtl: 86400 });
|
||||||
|
const recentAttacks = await getRecentAttacks(env.KV);
|
||||||
|
if (recentAttacks.length > 5) {
|
||||||
|
await setProtectionLevel(env.ZONE_ID, ProtectionLevel.HIGH, managedRulesetId, client);
|
||||||
|
return new Response("Protection increased");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Response("OK");
|
||||||
|
},
|
||||||
|
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
|
||||||
|
const recentAttacks = await getRecentAttacks(env.KV);
|
||||||
|
if (recentAttacks.length === 0) await setProtectionLevel(env.ZONE_ID, ProtectionLevel.MEDIUM, managedRulesetId, client);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-rule Tiered Protection (Enterprise Advanced)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const config = {
|
||||||
|
description: "Multi-tier DDoS protection",
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
expression: "not ip.src in $known_ips and not cf.bot_management.score gt 30",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expression: "cf.bot_management.verified_bot",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "medium", action: "managed_challenge" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expression: "ip.src in $trusted_ips",
|
||||||
|
action: "execute",
|
||||||
|
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "low" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defense in Depth
|
||||||
|
|
||||||
|
Layered security stack: DDoS + WAF + Rate Limiting + Bot Management.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Layer 1: DDoS (volumetric attacks)
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [{ expression: "true", action: "execute", action_parameters: { id: ddosRulesetId, overrides: { sensitivity_level: "medium" } } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer 2: WAF (exploit protection)
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("http_request_firewall_managed", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [{ expression: "true", action: "execute", action_parameters: { id: wafRulesetId } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer 3: Rate Limiting (abuse prevention)
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("http_ratelimit", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [{ expression: "http.request.uri.path eq \"/api/login\"", action: "block", ratelimit: { characteristics: ["ip.src"], period: 60, requests_per_period: 5 } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer 4: Bot Management (automation detection)
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("http_request_sbfm", {
|
||||||
|
zone_id: zoneId,
|
||||||
|
rules: [{ expression: "cf.bot_management.score lt 30", action: "managed_challenge" }],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Strategy for DDoS Mitigation
|
||||||
|
|
||||||
|
Exclude query strings from cache key to counter randomized query parameter attacks.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const cacheRule = {
|
||||||
|
expression: "http.request.uri.path matches \"^/api/\"",
|
||||||
|
action: "set_cache_settings",
|
||||||
|
action_parameters: {
|
||||||
|
cache: true,
|
||||||
|
cache_key: { ignore_query_strings_order: true, custom_key: { query_string: { exclude: { all: true } } } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.zones.rulesets.phases.entrypoint.update("http_request_cache_settings", { zone_id: zoneId, rules: [cacheRule] });
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale**: Attackers randomize query strings (`?random=123456`) to bypass cache. Excluding query params ensures cache hits absorb attack traffic.
|
||||||
|
|
||||||
|
See [configuration.md](./configuration.md) for rule structure details.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cloudflare Durable Objects Storage
|
||||||
|
|
||||||
|
Use SQLite for new classes. Existing KV-backed classes need their matching API reference; using key-value methods does not by itself identify the backend.
|
||||||
|
|
||||||
|
Fetch the relevant current documentation before implementing or reviewing changes.
|
||||||
|
|
||||||
|
| Task | Documentation |
|
||||||
|
|------|---------------|
|
||||||
|
| Choose SQL, key-value access, transactions, or recovery APIs | [SQLite storage API](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/); [Legacy KV storage API](https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/) |
|
||||||
|
| Configure the backend, class lifecycle, and placement | [Configuration](configuration.md) |
|
||||||
|
| Find operation semantics and storage options | [API routing](api.md) |
|
||||||
|
| Design schemas, caches, scheduled work, or cleanup | [Patterns](patterns.md) |
|
||||||
|
| Diagnose concurrency, limits, and billing | [Troubleshooting](gotchas.md) |
|
||||||
|
| Verify storage behavior in the Workers runtime | [Testing](testing.md) |
|
||||||
|
|
||||||
|
For object routing, WebSockets, and coordination design, see the [Durable Objects skill](../../../durable-objects/SKILL.md).
|
||||||
Loaded 100 of 1173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user